diff --git a/.github/dependabot.yml b/.github/dependabot.yml index be89e7a..855e314 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,13 @@ -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 + updates: - - package-ecosystem: "gomod" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: gomod + directory: / + schedule: + interval: daily + + - package-ecosystem: docker + directory: / schedule: - interval: "daily" + interval: daily diff --git a/client.go b/client.go index 4258c24..63adcb8 100644 --- a/client.go +++ b/client.go @@ -13,14 +13,14 @@ import ( const API_URL = "https://cloud-api.yandex.net/v1/disk/" -type Method string +type HttpMethod string const ( - GET Method = "GET" - POST Method = "POST" - PUT Method = "PUT" - PATCH Method = "PATCH" - DELETE Method = "DELETE" + GET HttpMethod = "GET" + POST HttpMethod = "POST" + PUT HttpMethod = "PUT" + PATCH HttpMethod = "PATCH" + DELETE HttpMethod = "DELETE" ) type Client struct { @@ -47,21 +47,28 @@ func New(token ...string) *Client { } } -func (c *Client) doRequest(ctx context.Context, method Method, resource string, body io.Reader) (*http.Response, error) { +func (c *Client) doRequest(ctx context.Context, method HttpMethod, resource string, data io.Reader) (*http.Response, error) { var resp *http.Response var err error - var data io.Reader + var body io.Reader - // ctx, cancel := context.WithCancel(ctx) + body = data - data = body + // todo: make time parameterized, not const + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(10*time.Second)) + defer cancel() if method == GET || method == DELETE { - data = nil + body = nil + } + + req, err := http.NewRequestWithContext(ctx, string(method), API_URL+resource, body) + if err != nil { + c.Logger.Fatal("error request", err) + return nil, err } - req, err := http.NewRequestWithContext(ctx, string(method), API_URL+resource, data) req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", "OAuth "+c.AccessToken) diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..1327f49 --- /dev/null +++ b/client_test.go @@ -0,0 +1,76 @@ +package disk + +import ( + "os" + "testing" + "time" +) + +func TestNew(t *testing.T) { + // Helper function to reset environment variable + resetEnv := func() { + os.Unsetenv("YANDEX_DISK_ACCESS_TOKEN") + } + + t.Run("With provided token", func(t *testing.T) { + resetEnv() + client := New("test-token") + if client == nil { + t.Fatal("Expected non-nil client") + } + if client.AccessToken != "test-token" { + t.Errorf("Expected AccessToken to be 'test-token', got '%s'", client.AccessToken) + } + if client.HTTPClient == nil { + t.Fatal("Expected non-nil HTTPClient") + } + if client.HTTPClient.Timeout != 10*time.Second { + t.Errorf("Expected Timeout to be 10 seconds, got %v", client.HTTPClient.Timeout) + } + }) + + t.Run("With environment variable", func(t *testing.T) { + resetEnv() + os.Setenv("YANDEX_DISK_ACCESS_TOKEN", "env-token") + client := New() + if client == nil { + t.Fatal("Expected non-nil client") + } + if client.AccessToken != "env-token" { + t.Errorf("Expected AccessToken to be 'env-token', got '%s'", client.AccessToken) + } + }) + + t.Run("Without token and empty environment variable", func(t *testing.T) { + resetEnv() + client := New() + if client != nil { + t.Fatal("Expected nil client") + } + }) + + t.Run("With multiple tokens", func(t *testing.T) { + resetEnv() + client := New("token1", "token2") + if client == nil { + t.Fatal("Expected non-nil client") + } + if client.AccessToken != "token1" { + t.Errorf("Expected AccessToken to be 'token1', got '%s'", client.AccessToken) + } + }) + + t.Run("HTTPClient configuration", func(t *testing.T) { + resetEnv() + client := New("test-token") + if client == nil { + t.Fatal("Expected non-nil client") + } + if client.HTTPClient == nil { + t.Fatal("Expected non-nil HTTPClient") + } + if client.HTTPClient.Timeout != 10*time.Second { + t.Errorf("Expected Timeout to be 10 seconds, got %v", client.HTTPClient.Timeout) + } + }) +} diff --git a/disk_test.go b/disk_test.go index 26fd35b..d93c97b 100644 --- a/disk_test.go +++ b/disk_test.go @@ -3,10 +3,10 @@ package disk import ( "context" "crypto/tls" - "io/ioutil" "net" "net/http" "net/http/httptest" + "os" "testing" "github.com/stretchr/testify/assert" @@ -32,7 +32,7 @@ func testingHTTPClient(handler http.Handler) (*http.Client, func()) { } func loadTestResponse(actionName string) []byte { - response, _ := ioutil.ReadFile(TEST_DATA_DIR + actionName + ".json") + response, _ := os.ReadFile(TEST_DATA_DIR + actionName + ".json") return response } diff --git a/helpers.go b/helpers.go index 6969abf..a6398be 100644 --- a/helpers.go +++ b/helpers.go @@ -1,31 +1,25 @@ package disk -import ( - "encoding/json" - "log" -) +import "log" -func prettyPrint(data interface{}) []byte { - result, err := json.MarshalIndent(data, "", " ") - if haveError(err) { - log.Fatal(err) - } - return result -} - -func haveError(err error) bool { +// handleError is a helper function to handle errors +// and exit the program if an error occurs +func handleError(err error) { if err != nil { - log.Fatal(err) - return true + log.Fatal("Error:", err) } - return false } func inArray(n int, array []int) bool { + if len(array) == 0 { + return false + } + + set := make(map[int]struct{}, len(array)) for _, b := range array { - if b == n { - return true - } + set[b] = struct{}{} } - return false + + _, exists := set[n] + return exists } diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..da432bc --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,183 @@ +package disk + +import ( + "bytes" + "log" + "testing" +) + +// Mock for os.Exit +var osExitCalled = false +var osExitCode = 0 +var osExit = func(code int) { + osExitCalled = true + osExitCode = code + panic("os.Exit called") +} + +func TestInArray(t *testing.T) { + tests := []struct { + name string + n int + array []int + expected bool + }{ + { + name: "Empty array", + n: 5, + array: []int{}, + expected: false, + }, + { + name: "Single element array, element present", + n: 5, + array: []int{5}, + expected: true, + }, + { + name: "Single element array, element not present", + n: 5, + array: []int{3}, + expected: false, + }, + { + name: "Multiple elements, element present", + n: 5, + array: []int{1, 2, 3, 4, 5, 6, 7}, + expected: true, + }, + { + name: "Multiple elements, element not present", + n: 10, + array: []int{1, 2, 3, 4, 5, 6, 7}, + expected: false, + }, + { + name: "Duplicate elements, element present", + n: 5, + array: []int{1, 5, 2, 5, 3, 5, 4}, + expected: true, + }, + { + name: "Large array, element present", + n: 999, + array: func() []int { + arr := make([]int, 1000) + for i := range arr { + arr[i] = i + } + return arr + }(), + expected: true, + }, + { + name: "Large array, element not present", + n: 1000, + array: func() []int { + arr := make([]int, 1000) + for i := range arr { + arr[i] = i + } + return arr + }(), + expected: false, + }, + { + name: "Negative numbers, element present", + n: -5, + array: []int{-7, -6, -5, -4, -3}, + expected: true, + }, + { + name: "Negative numbers, element not present", + n: -8, + array: []int{-7, -6, -5, -4, -3}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := inArray(tt.n, tt.array) + if result != tt.expected { + t.Errorf("inArray(%d, %v) = %v; want %v", tt.n, tt.array, result, tt.expected) + } + }) + } +} + +func TestHandleError(t *testing.T) { + + // Save the original log output and flags + originalOutput := log.Writer() + originalFlags := log.Flags() + defer func() { + // Restore the original log output and flags after the test + log.SetOutput(originalOutput) + log.SetFlags(originalFlags) + }() + + // Create a buffer to capture log output + var buf bytes.Buffer + log.SetOutput(&buf) + + // Remove timestamp from log output for easier testing + log.SetFlags(0) + + // Override os.Exit to prevent the test from terminating + originalOsExit := osExit + defer func() { osExit = originalOsExit }() + var exitCode int + osExit = func(code int) { + exitCode = code + panic("os.Exit called") + } + + tests := []struct { + name string + err error + expectedLog string + expectedPanic bool + }{ + { + name: "Nil error", + err: nil, + expectedLog: "", + expectedPanic: false, + }, + // todo + // { + // name: "Non-nil error", + // err: errors.New("test error"), + // expectedLog: "Error: test error\n", + // expectedPanic: true, + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear the buffer before each test + buf.Reset() + exitCode = 0 + + // Use a function to capture panics + func() { + defer func() { + r := recover() + if (r != nil) != tt.expectedPanic { + t.Errorf("handleError() panic = %v, expectedPanic %v", r, tt.expectedPanic) + } + if r != nil && exitCode != 1 { + t.Errorf("Expected exit code 1, got %d", exitCode) + } + }() + handleError(tt.err) + }() + + // Check the log output + if got := buf.String(); got != tt.expectedLog { + t.Errorf("handleError() log = %q, want %q", got, tt.expectedLog) + } + }) + } +} diff --git a/public.go b/public.go index 7d59cee..49bc8b4 100644 --- a/public.go +++ b/public.go @@ -13,16 +13,13 @@ func (c *Client) GetMetadataForPublicResource(ctx context.Context, public_key st var decoded *json.Decoder resp, err := c.doRequest(ctx, GET, "public/resources?public_key="+public_key, nil) - if haveError(err) { - log.Fatal("Request failed") - } + handleError(err) if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) - } + handleError(err) + return nil, errorResponse } @@ -41,16 +38,12 @@ func (c *Client) GetDownloadURLForPublicResource(ctx context.Context, public_key var decoded *json.Decoder resp, err := c.doRequest(ctx, GET, "public/resources/download?public_key="+public_key, nil) - if haveError(err) { - log.Fatal("Request failed") - } + handleError(err) if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) - } + handleError(err) return nil, errorResponse } @@ -69,9 +62,7 @@ func (c *Client) SavePublicResource(ctx context.Context, public_key string) (*Li var decoded *json.Decoder resp, err := c.doRequest(ctx, POST, "public/resources/save-to-disk?public_key="+public_key, nil) - if haveError(err) { - log.Fatal("Request failed") - } + handleError(err) // Если сохранение происходит асинхронно, // то вернёт ответ с кодом 202 и ссылкой на асинхронную операцию. @@ -79,9 +70,7 @@ func (c *Client) SavePublicResource(ctx context.Context, public_key string) (*Li if !inArray(resp.StatusCode, []int{200, 201, 202}) { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) - } + handleError(err) return nil, errorResponse } diff --git a/resources.go b/resources.go index cbf2baa..dba451c 100644 --- a/resources.go +++ b/resources.go @@ -25,8 +25,8 @@ func (c *Client) DeleteResource(ctx context.Context, path string, permanently bo } resp, err := c.doRequest(ctx, DELETE, url, nil) - if haveError(err) { - log.Fatal(err) + if err != nil { + handleError(err) return err } @@ -46,9 +46,7 @@ func (c *Client) GetMetadata(ctx context.Context, path string) (*Resource, *Erro var decoded *json.Decoder resp, err := c.doRequest(ctx, GET, "resources?path="+path, nil) - if haveError(err) { - log.Fatal("Request failed") - } + handleError(err) if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) @@ -67,14 +65,16 @@ func (c *Client) GetMetadata(ctx context.Context, path string) (*Resource, *Erro return resource, nil } -/* todo: add examples to README -newMeta := map[string]map[string]string{ - "custom_properties": { - "key_01": "value_01", - "key_02": "value_02", - "key_07": "value_07", - }, -} +/* +todo: add examples to README + + newMeta := map[string]map[string]string{ + "custom_properties": { + "key_01": "value_01", + "key_02": "value_02", + "key_07": "value_07", + }, + } */ func (c *Client) UpdateMetadata(ctx context.Context, path string, custom_properties map[string]map[string]string) (*Resource, *ErrorResponse) { if len(path) < 1 { @@ -90,14 +90,10 @@ func (c *Client) UpdateMetadata(ctx context.Context, path string, custom_propert body, err = json.Marshal(custom_properties) - if haveError(err) { - log.Fatal("payload error") - } + handleError(err) resp, err := c.doRequest(ctx, PATCH, "resources?path="+path, bytes.NewBuffer([]byte(body))) - if haveError(err) { - log.Fatal("Request failed") - } + handleError(err) if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) @@ -116,7 +112,7 @@ func (c *Client) UpdateMetadata(ctx context.Context, path string, custom_propert return resource, nil } -// CreateDir creates a new dorectory with 'path'(string) name +// CreateDir creates a new directory with the specified 'path' name. // todo: can't create nested dirs like newDir/subDir/anotherDir func (c *Client) CreateDir(ctx context.Context, path string) (*Link, *ErrorResponse) { if len(path) < 1 { @@ -129,8 +125,8 @@ func (c *Client) CreateDir(ctx context.Context, path string) (*Link, *ErrorRespo var decoded *json.Decoder resp, err := c.doRequest(ctx, PUT, "resources?path="+path, nil) - if haveError(err) { - log.Fatal("Request failed") + if err != nil { + handleError(err) return nil, nil } @@ -163,9 +159,7 @@ func (c *Client) CopyResource(ctx context.Context, from, path string) (*Link, *E var decoded *json.Decoder resp, err := c.doRequest(ctx, POST, "resources/copy?from="+from+"&path="+path, nil) - if haveError(err) { - log.Fatal("Request failed") - } + handleError(err) if !inArray(resp.StatusCode, []int{200, 201, 202}) { decoded = json.NewDecoder(resp.Body) @@ -195,16 +189,12 @@ func (c *Client) GetDownloadURL(ctx context.Context, path string) (*Link, *Error var decoded *json.Decoder resp, err := c.doRequest(ctx, GET, "resources/download?path="+path, nil) - if haveError(err) { - log.Fatal("Request failed") - } + handleError(err) if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) - } + handleError(err) return nil, errorResponse } @@ -224,16 +214,14 @@ func (c *Client) GetSortedFiles(ctx context.Context) (*FilesResourceList, *Error var decoded *json.Decoder resp, err := c.doRequest(ctx, GET, "resources/files", nil) - if haveError(err) { - log.Fatal("Request failed") + if err != nil { + handleError(err) } if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) - } + handleError(err) return nil, errorResponse } @@ -254,15 +242,15 @@ func (c *Client) GetLastUploadedResources(ctx context.Context) (*LastUploadedRes var decoded *json.Decoder resp, err := c.doRequest(ctx, GET, "resources/last-uploaded", nil) - if haveError(err) { - log.Fatal("Request failed") + if err != nil { + handleError(err) } if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) + if err != nil { + handleError(err) } return nil, errorResponse } @@ -284,15 +272,15 @@ func (c *Client) MoveResource(ctx context.Context, from, path string) (*Link, *E var decoded *json.Decoder resp, err := c.doRequest(ctx, POST, "resources/move?from="+from+"&path="+path, nil) - if haveError(err) { - log.Fatal("Request failed") + if err != nil { + handleError(err) } if !inArray(resp.StatusCode, []int{201, 202}) { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) + if err != nil { + handleError(err) } return nil, errorResponse } @@ -312,15 +300,15 @@ func (c *Client) GetPublicResources(ctx context.Context) (*PublicResourcesList, var decoded *json.Decoder resp, err := c.doRequest(ctx, GET, "resources/public", nil) - if haveError(err) { - log.Fatal("Request failed") + if err != nil { + handleError(err) } if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) + if err != nil { + handleError(err) } return nil, errorResponse } @@ -340,15 +328,15 @@ func (c *Client) PublishResource(ctx context.Context, path string) (*Link, *Erro var decoded *json.Decoder resp, err := c.doRequest(ctx, PUT, "resources/publish?path="+path, nil) - if haveError(err) { - log.Fatal("Request failed") + if err != nil { + handleError(err) } if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) + if err != nil { + handleError(err) } return nil, errorResponse } @@ -368,15 +356,15 @@ func (c *Client) UnpublishResource(ctx context.Context, path string) (*Link, *Er var decoded *json.Decoder resp, err := c.doRequest(ctx, PUT, "resources/unpublish?path="+path, nil) - if haveError(err) { - log.Fatal("Request failed") + if err != nil { + handleError(err) } if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) + if err != nil { + handleError(err) } return nil, errorResponse } @@ -396,15 +384,15 @@ func (c *Client) GetLinkForUpload(ctx context.Context, path string) (*ResourceUp var decoded *json.Decoder resp, err := c.doRequest(ctx, GET, "resources/upload?path="+path, nil) - if haveError(err) { - log.Fatal("Request failed") + if err != nil { + handleError(err) } if resp.StatusCode != 200 { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) + if err != nil { + handleError(err) } return nil, errorResponse } @@ -425,15 +413,15 @@ func (c *Client) UploadFile(ctx context.Context, path, url string) (*Link, *Erro var decoded *json.Decoder resp, err := c.doRequest(ctx, POST, "resources/upload?path="+path+"&url="+url, nil) - if haveError(err) { - log.Fatal("Request failed") + if err != nil { + handleError(err) } if !inArray(resp.StatusCode, []int{200, 202}) { decoded = json.NewDecoder(resp.Body) err := decoded.Decode(&errorResponse) - if haveError(err) { - log.Fatal(err) + if err != nil { + handleError(err) } return nil, errorResponse }