diff --git a/README.md b/README.md index a45e45a..eebe0b7 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,8 @@ backups, err := c.ListBackups(ctx) ### Available Methods +The SDK sends read-only API calls with `GET` query parameters and state-changing API calls with `POST application/x-www-form-urlencoded` form data, so write parameters and credentials are not placed in the URL. + **Server Management**: `GetServiceInfo`, `GetLiveServiceInfo`, `Start`, `Stop`, `Restart`, `Kill`, `SetHostname`, `ReinstallOS`, `ResetRootPassword`, `MountISO`, `UnmountISO` **Monitoring**: `GetRawUsageStats`, `GetAuditLog`, `GetRateLimitStatus` diff --git a/README.zh.md b/README.zh.md index d18f8e6..689ff4e 100644 --- a/README.zh.md +++ b/README.zh.md @@ -100,6 +100,8 @@ backups, err := c.ListBackups(ctx) ### 可用方法 +SDK 只读 API 调用使用 `GET` query 参数;会改变状态的写 API 调用使用 `POST application/x-www-form-urlencoded` 表单数据,因此写接口参数和凭据不会放在 URL 中。 + **服务器管理**: `GetServiceInfo`、`GetLiveServiceInfo`、`Start`、`Stop`、`Restart`、`Kill`、`SetHostname`、`ReinstallOS`、`ResetRootPassword`、`MountISO`、`UnmountISO` **监控**: `GetRawUsageStats`、`GetAuditLog`、`GetRateLimitStatus` diff --git a/pkg/client/client.go b/pkg/client/client.go index badb681..1ecefa2 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -70,7 +70,7 @@ func (c *Client) CreateSnapshot(ctx context.Context, description string) (*Creat } var resp CreateSnapshotResponse - if err := c.doRequest(ctx, "snapshot/create", params, &resp); err != nil { + if err := c.doPostRequest(ctx, "snapshot/create", params, &resp); err != nil { return nil, err } @@ -90,7 +90,7 @@ func (c *Client) ListSnapshots(ctx context.Context) (*SnapshotListResponse, erro // DeleteSnapshot deletes a snapshot by fileName func (c *Client) DeleteSnapshot(ctx context.Context, fileName string) error { var resp BaseResponse - if err := c.doRequest(ctx, "snapshot/delete", map[string]string{"snapshot": fileName}, &resp); err != nil { + if err := c.doPostRequest(ctx, "snapshot/delete", map[string]string{"snapshot": fileName}, &resp); err != nil { return err } @@ -100,7 +100,7 @@ func (c *Client) DeleteSnapshot(ctx context.Context, fileName string) error { // RestoreSnapshot restores a snapshot by fileName (overwrites all data on VPS) func (c *Client) RestoreSnapshot(ctx context.Context, fileName string) error { var resp BaseResponse - if err := c.doRequest(ctx, "snapshot/restore", map[string]string{"snapshot": fileName}, &resp); err != nil { + if err := c.doPostRequest(ctx, "snapshot/restore", map[string]string{"snapshot": fileName}, &resp); err != nil { return err } @@ -115,7 +115,7 @@ func (c *Client) ToggleSnapshotSticky(ctx context.Context, fileName string, stic } var resp BaseResponse - if err := c.doRequest(ctx, "snapshot/toggleSticky", map[string]string{ + if err := c.doPostRequest(ctx, "snapshot/toggleSticky", map[string]string{ "snapshot": fileName, "sticky": stickyStr, }, &resp); err != nil { @@ -128,7 +128,7 @@ func (c *Client) ToggleSnapshotSticky(ctx context.Context, fileName string, stic // ExportSnapshot generates a token for transferring snapshot to another instance func (c *Client) ExportSnapshot(ctx context.Context, fileName string) (*SnapshotExportResponse, error) { var resp SnapshotExportResponse - if err := c.doRequest(ctx, "snapshot/export", map[string]string{"snapshot": fileName}, &resp); err != nil { + if err := c.doPostRequest(ctx, "snapshot/export", map[string]string{"snapshot": fileName}, &resp); err != nil { return nil, err } @@ -138,7 +138,7 @@ func (c *Client) ExportSnapshot(ctx context.Context, fileName string) (*Snapshot // ImportSnapshot imports a snapshot from another instance using VEID and token func (c *Client) ImportSnapshot(ctx context.Context, sourceVeid, sourceToken string) error { var resp BaseResponse - if err := c.doRequest(ctx, "snapshot/import", map[string]string{ + if err := c.doPostRequest(ctx, "snapshot/import", map[string]string{ "sourceVeid": sourceVeid, "sourceToken": sourceToken, }, &resp); err != nil { @@ -151,7 +151,7 @@ func (c *Client) ImportSnapshot(ctx context.Context, sourceVeid, sourceToken str // Restart restarts the VPS func (c *Client) Restart(ctx context.Context) error { var resp BaseResponse - if err := c.doRequest(ctx, "restart", nil, &resp); err != nil { + if err := c.doPostRequest(ctx, "restart", nil, &resp); err != nil { return err } @@ -161,7 +161,7 @@ func (c *Client) Restart(ctx context.Context) error { // Start starts the VPS func (c *Client) Start(ctx context.Context) error { var resp BaseResponse - if err := c.doRequest(ctx, "start", nil, &resp); err != nil { + if err := c.doPostRequest(ctx, "start", nil, &resp); err != nil { return err } @@ -171,7 +171,7 @@ func (c *Client) Start(ctx context.Context) error { // Stop stops the VPS func (c *Client) Stop(ctx context.Context) error { var resp BaseResponse - if err := c.doRequest(ctx, "stop", nil, &resp); err != nil { + if err := c.doPostRequest(ctx, "stop", nil, &resp); err != nil { return err } @@ -182,7 +182,7 @@ func (c *Client) Stop(ctx context.Context) error { // Please use this feature with great care as any unsaved data will be lost. func (c *Client) Kill(ctx context.Context) error { var resp BaseResponse - if err := c.doRequest(ctx, "kill", nil, &resp); err != nil { + if err := c.doPostRequest(ctx, "kill", nil, &resp); err != nil { return err } @@ -203,7 +203,7 @@ func (c *Client) GetAvailableOS(ctx context.Context) (*AvailableOSResponse, erro // WARNING: This will destroy all data on the VPS! func (c *Client) ReinstallOS(ctx context.Context, osTemplate string) error { var resp BaseResponse - if err := c.doRequest(ctx, "reinstallOS", map[string]string{"os": osTemplate}, &resp); err != nil { + if err := c.doPostRequest(ctx, "reinstallOS", map[string]string{"os": osTemplate}, &resp); err != nil { return err } @@ -233,7 +233,7 @@ func (c *Client) GetAuditLog(ctx context.Context) (*AuditLogResponse, error) { // ResetRootPassword resets the root password and returns the new password func (c *Client) ResetRootPassword(ctx context.Context) (*ResetRootPasswordResponse, error) { var resp ResetRootPasswordResponse - if err := c.doRequest(ctx, "resetRootPassword", nil, &resp); err != nil { + if err := c.doPostRequest(ctx, "resetRootPassword", nil, &resp); err != nil { return nil, err } @@ -247,13 +247,7 @@ func (c *Client) doRequest(ctx context.Context, endpoint string, params map[stri return fmt.Errorf("failed to parse URL: %w", err) } - q := u.Query() - q.Set("veid", c.veid) - q.Set("api_key", c.apiKey) - - for k, v := range params { - q.Set(k, v) - } + q := c.requestValues(params) u.RawQuery = q.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) @@ -261,10 +255,41 @@ func (c *Client) doRequest(ctx context.Context, endpoint string, params map[stri return fmt.Errorf("failed to create request: %w", err) } + return c.executeRequest(c.httpClient, req, result) +} + +// doPostRequest performs a generic API POST form request. +func (c *Client) doPostRequest(ctx context.Context, endpoint string, params map[string]string, result any) error { + u, err := url.Parse(c.baseURL + "/" + endpoint) + if err != nil { + return fmt.Errorf("failed to parse URL: %w", err) + } + + form := c.requestValues(params) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(form.Encode())) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + return c.executeRequest(c.httpClient, req, result) +} + +func (c *Client) requestValues(params map[string]string) url.Values { + values := url.Values{} + values.Set("veid", c.veid) + values.Set("api_key", c.apiKey) + for k, v := range params { + values.Set(k, v) + } + return values +} + +func (c *Client) executeRequest(httpClient *http.Client, req *http.Request, result any) error { req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", version.GetUserAgent()) - resp, err := c.httpClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("failed to send request: %w", err) } @@ -294,7 +319,7 @@ func (c *Client) ListBackups(ctx context.Context) (*BackupListResponse, error) { // CopyBackupToSnapshot copies a backup to a restorable snapshot func (c *Client) CopyBackupToSnapshot(ctx context.Context, backupToken string) error { var resp BaseResponse - if err := c.doRequest(ctx, "backup/copyToSnapshot", map[string]string{ + if err := c.doPostRequest(ctx, "backup/copyToSnapshot", map[string]string{ "backupToken": backupToken, }, &resp); err != nil { return err @@ -306,7 +331,7 @@ func (c *Client) CopyBackupToSnapshot(ctx context.Context, backupToken string) e // SetHostname sets a new hostname for the VPS func (c *Client) SetHostname(ctx context.Context, newHostname string) error { var resp BaseResponse - if err := c.doRequest(ctx, "setHostname", map[string]string{ + if err := c.doPostRequest(ctx, "setHostname", map[string]string{ "newHostname": newHostname, }, &resp); err != nil { return err @@ -362,7 +387,7 @@ func (c *Client) Unsuspend(ctx context.Context, recordID int) error { } var resp BaseResponse - if err := c.doRequest(ctx, "unsuspend", map[string]string{ + if err := c.doPostRequest(ctx, "unsuspend", map[string]string{ "record_id": fmt.Sprintf("%d", recordID), }, &resp); err != nil { return err @@ -378,7 +403,7 @@ func (c *Client) ResolvePolicyViolation(ctx context.Context, recordID int) error } var resp BaseResponse - if err := c.doRequest(ctx, "resolvePolicyViolation", map[string]string{ + if err := c.doPostRequest(ctx, "resolvePolicyViolation", map[string]string{ "record_id": fmt.Sprintf("%d", recordID), }, &resp); err != nil { return err @@ -395,7 +420,7 @@ func (c *Client) SetNotificationPreferences(ctx context.Context, preferences map } var resp SetNotificationPreferencesResponse - if err := c.doRequest(ctx, "kiwivm/setNotificationPreferences", map[string]string{ + if err := c.doPostRequest(ctx, "kiwivm/setNotificationPreferences", map[string]string{ "json_notification_preferences": encoded, }, &resp); err != nil { return nil, err @@ -461,7 +486,7 @@ func (c *Client) UpdateSshKeys(ctx context.Context, sshKeys []string) error { } var resp BaseResponse - if err := c.doRequest(ctx, "updateSshKeys", params, &resp); err != nil { + if err := c.doPostRequest(ctx, "updateSshKeys", params, &resp); err != nil { return err } @@ -471,7 +496,7 @@ func (c *Client) UpdateSshKeys(ctx context.Context, sshKeys []string) error { // SetPTR sets new PTR (rDNS) record for IP address func (c *Client) SetPTR(ctx context.Context, ip, ptr string) error { var resp BaseResponse - if err := c.doRequest(ctx, "setPTR", map[string]string{ + if err := c.doPostRequest(ctx, "setPTR", map[string]string{ "ip": ip, "ptr": ptr, }, &resp); err != nil { @@ -485,7 +510,7 @@ func (c *Client) SetPTR(ctx context.Context, ip, ptr string) error { // VM must be completely shut down and restarted after this API call func (c *Client) MountISO(ctx context.Context, iso string) error { var resp BaseResponse - if err := c.doRequest(ctx, "iso/mount", map[string]string{ + if err := c.doPostRequest(ctx, "iso/mount", map[string]string{ "iso": iso, }, &resp); err != nil { return err @@ -498,7 +523,7 @@ func (c *Client) MountISO(ctx context.Context, iso string) error { // VM must be completely shut down and restarted after this API call func (c *Client) UnmountISO(ctx context.Context) error { var resp BaseResponse - if err := c.doRequest(ctx, "iso/unmount", nil, &resp); err != nil { + if err := c.doPostRequest(ctx, "iso/unmount", nil, &resp); err != nil { return err } @@ -528,57 +553,33 @@ func (c *Client) StartMigrationWithTimeout(ctx context.Context, locationID strin "location": locationID, } var resp MigrateStartResponse - if err := c.doRequestWithTimeout(ctx, "migrate/start", params, &resp, timeout); err != nil { + if err := c.doPostRequestWithTimeout(ctx, "migrate/start", params, &resp, timeout); err != nil { return nil, err } return wrapErrorWithBase(&resp, resp.BaseResponse) } -// doRequestWithTimeout performs a generic API request using a custom timeout for long-running operations -func (c *Client) doRequestWithTimeout(ctx context.Context, endpoint string, params map[string]string, result any, timeout time.Duration) error { +// doPostRequestWithTimeout performs a generic POST form request using a custom timeout. +func (c *Client) doPostRequestWithTimeout(ctx context.Context, endpoint string, params map[string]string, result any, timeout time.Duration) error { u, err := url.Parse(c.baseURL + "/" + endpoint) if err != nil { return fmt.Errorf("failed to parse URL: %w", err) } - q := u.Query() - q.Set("veid", c.veid) - q.Set("api_key", c.apiKey) - - for k, v := range params { - q.Set(k, v) - } - u.RawQuery = q.Encode() - // Apply context deadline as well as client timeout ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout) defer cancel() - req, err := http.NewRequestWithContext(ctxWithTimeout, http.MethodGet, u.String(), nil) + form := c.requestValues(params) + req, err := http.NewRequestWithContext(ctxWithTimeout, http.MethodPost, u.String(), strings.NewReader(form.Encode())) if err != nil { return fmt.Errorf("failed to create request: %w", err) } - - req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", version.GetUserAgent()) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") customClient := &http.Client{Timeout: timeout} - resp, err := customClient.Do(req) - if err != nil { - return fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() //nolint:errcheck - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("API request failed with status: %d %s", resp.StatusCode, resp.Status) - } - - if err := json.NewDecoder(resp.Body).Decode(result); err != nil { - return fmt.Errorf("failed to decode response: %w", err) - } - - return nil + return c.executeRequest(customClient, req, result) } // wrapError wraps a response with error checking - returns (result, error) @@ -634,7 +635,7 @@ func wrapOnlyErrorFromBase(base BaseResponse) error { // AddIPv6 assigns a new IPv6 /64 subnet to the VPS func (c *Client) AddIPv6(ctx context.Context) (*IPv6AddResponse, error) { var resp IPv6AddResponse - if err := c.doRequest(ctx, "ipv6/add", nil, &resp); err != nil { + if err := c.doPostRequest(ctx, "ipv6/add", nil, &resp); err != nil { return nil, err } @@ -648,7 +649,7 @@ func (c *Client) DeleteIPv6(ctx context.Context, subnet string) error { } var resp BaseResponse - if err := c.doRequest(ctx, "ipv6/delete", params, &resp); err != nil { + if err := c.doPostRequest(ctx, "ipv6/delete", params, &resp); err != nil { return err } @@ -673,7 +674,7 @@ func (c *Client) AssignPrivateIP(ctx context.Context, ip string) (*PrivateIPAssi } var resp PrivateIPAssignResponse - if err := c.doRequest(ctx, "privateIp/assign", params, &resp); err != nil { + if err := c.doPostRequest(ctx, "privateIp/assign", params, &resp); err != nil { return nil, err } @@ -687,7 +688,7 @@ func (c *Client) DeletePrivateIP(ctx context.Context, ip string) error { } var resp BaseResponse - if err := c.doRequest(ctx, "privateIp/delete", params, &resp); err != nil { + if err := c.doPostRequest(ctx, "privateIp/delete", params, &resp); err != nil { return err } diff --git a/pkg/client/write_methods_test.go b/pkg/client/write_methods_test.go index 9b30b53..7af3f2f 100644 --- a/pkg/client/write_methods_test.go +++ b/pkg/client/write_methods_test.go @@ -5,9 +5,277 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" + "time" ) +func TestClient_WriteMethodsUsePostForm(t *testing.T) { + tests := []struct { + name string + endpoint string + call func(context.Context, *Client) error + wantForm map[string]string + }{ + { + name: "create snapshot", + endpoint: "snapshot/create", + call: func(ctx context.Context, c *Client) error { + _, err := c.CreateSnapshot(ctx, "backup-name") + return err + }, + wantForm: map[string]string{"description": "backup-name"}, + }, + { + name: "delete snapshot", + endpoint: "snapshot/delete", + call: func(ctx context.Context, c *Client) error { + return c.DeleteSnapshot(ctx, "snapshot.tar.gz") + }, + wantForm: map[string]string{"snapshot": "snapshot.tar.gz"}, + }, + { + name: "restore snapshot", + endpoint: "snapshot/restore", + call: func(ctx context.Context, c *Client) error { + return c.RestoreSnapshot(ctx, "snapshot.tar.gz") + }, + wantForm: map[string]string{"snapshot": "snapshot.tar.gz"}, + }, + { + name: "toggle snapshot sticky", + endpoint: "snapshot/toggleSticky", + call: func(ctx context.Context, c *Client) error { + return c.ToggleSnapshotSticky(ctx, "snapshot.tar.gz", true) + }, + wantForm: map[string]string{"snapshot": "snapshot.tar.gz", "sticky": "1"}, + }, + { + name: "export snapshot", + endpoint: "snapshot/export", + call: func(ctx context.Context, c *Client) error { + _, err := c.ExportSnapshot(ctx, "snapshot.tar.gz") + return err + }, + wantForm: map[string]string{"snapshot": "snapshot.tar.gz"}, + }, + { + name: "import snapshot", + endpoint: "snapshot/import", + call: func(ctx context.Context, c *Client) error { + return c.ImportSnapshot(ctx, "654321", "token") + }, + wantForm: map[string]string{"sourceVeid": "654321", "sourceToken": "token"}, + }, + { + name: "restart", + endpoint: "restart", + call: func(ctx context.Context, c *Client) error { + return c.Restart(ctx) + }, + }, + { + name: "start", + endpoint: "start", + call: func(ctx context.Context, c *Client) error { + return c.Start(ctx) + }, + }, + { + name: "stop", + endpoint: "stop", + call: func(ctx context.Context, c *Client) error { + return c.Stop(ctx) + }, + }, + { + name: "kill", + endpoint: "kill", + call: func(ctx context.Context, c *Client) error { + return c.Kill(ctx) + }, + }, + { + name: "reinstall os", + endpoint: "reinstallOS", + call: func(ctx context.Context, c *Client) error { + return c.ReinstallOS(ctx, "debian-12-x86_64") + }, + wantForm: map[string]string{"os": "debian-12-x86_64"}, + }, + { + name: "reset root password", + endpoint: "resetRootPassword", + call: func(ctx context.Context, c *Client) error { + _, err := c.ResetRootPassword(ctx) + return err + }, + }, + { + name: "copy backup to snapshot", + endpoint: "backup/copyToSnapshot", + call: func(ctx context.Context, c *Client) error { + return c.CopyBackupToSnapshot(ctx, "backup-token") + }, + wantForm: map[string]string{"backupToken": "backup-token"}, + }, + { + name: "set hostname", + endpoint: "setHostname", + call: func(ctx context.Context, c *Client) error { + return c.SetHostname(ctx, "host.example.com") + }, + wantForm: map[string]string{"newHostname": "host.example.com"}, + }, + { + name: "unsuspend", + endpoint: "unsuspend", + call: func(ctx context.Context, c *Client) error { + return c.Unsuspend(ctx, 123) + }, + wantForm: map[string]string{"record_id": "123"}, + }, + { + name: "resolve policy violation", + endpoint: "resolvePolicyViolation", + call: func(ctx context.Context, c *Client) error { + return c.ResolvePolicyViolation(ctx, 789) + }, + wantForm: map[string]string{"record_id": "789"}, + }, + { + name: "set notification preferences", + endpoint: "kiwivm/setNotificationPreferences", + call: func(ctx context.Context, c *Client) error { + _, err := c.SetNotificationPreferences(ctx, map[string]bool{"security-successful-login": true}) + return err + }, + wantForm: map[string]string{"json_notification_preferences": `{"security-successful-login":1}`}, + }, + { + name: "update ssh keys", + endpoint: "updateSshKeys", + call: func(ctx context.Context, c *Client) error { + return c.UpdateSshKeys(ctx, []string{"ssh-rsa key1", "ssh-ed25519 key2"}) + }, + wantForm: map[string]string{"ssh_keys": "ssh-rsa key1\nssh-ed25519 key2\n"}, + }, + { + name: "set ptr", + endpoint: "setPTR", + call: func(ctx context.Context, c *Client) error { + return c.SetPTR(ctx, "192.0.2.10", "host.example.com") + }, + wantForm: map[string]string{"ip": "192.0.2.10", "ptr": "host.example.com"}, + }, + { + name: "mount iso", + endpoint: "iso/mount", + call: func(ctx context.Context, c *Client) error { + return c.MountISO(ctx, "ubuntu.iso") + }, + wantForm: map[string]string{"iso": "ubuntu.iso"}, + }, + { + name: "unmount iso", + endpoint: "iso/unmount", + call: func(ctx context.Context, c *Client) error { + return c.UnmountISO(ctx) + }, + }, + { + name: "start migration with timeout", + endpoint: "migrate/start", + call: func(ctx context.Context, c *Client) error { + _, err := c.StartMigrationWithTimeout(ctx, "us-west", time.Second) + return err + }, + wantForm: map[string]string{"location": "us-west"}, + }, + { + name: "add ipv6", + endpoint: "ipv6/add", + call: func(ctx context.Context, c *Client) error { + _, err := c.AddIPv6(ctx) + return err + }, + }, + { + name: "delete ipv6", + endpoint: "ipv6/delete", + call: func(ctx context.Context, c *Client) error { + return c.DeleteIPv6(ctx, "2001:db8::/64") + }, + wantForm: map[string]string{"ip": "2001:db8::/64"}, + }, + { + name: "assign private ip", + endpoint: "privateIp/assign", + call: func(ctx context.Context, c *Client) error { + _, err := c.AssignPrivateIP(ctx, "10.0.0.2") + return err + }, + wantForm: map[string]string{"ip": "10.0.0.2"}, + }, + { + name: "delete private ip", + endpoint: "privateIp/delete", + call: func(ctx context.Context, c *Client) error { + return c.DeletePrivateIP(ctx, "10.0.0.2") + }, + wantForm: map[string]string{"ip": "10.0.0.2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertPostForm(t, r, tt.endpoint, "valid_key", tt.wantForm) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"error":0}`)) + })) + defer server.Close() + + c := NewClient("valid_key", "123456") + c.SetBaseURL(server.URL) + + if err := tt.call(context.Background(), c); err != nil { + t.Fatalf("%s error = %v", tt.name, err) + } + }) + } +} + +func TestClient_ReadMethodsUseGetQuery(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Fatalf("method = %s, want GET", r.Method) + } + if got := r.URL.Query().Get("veid"); got != "123456" { + t.Fatalf("query veid = %q, want 123456", got) + } + if got := r.URL.Query().Get("api_key"); got != "valid_key" { + t.Fatalf("query api_key = %q, want valid_key", got) + } + if err := r.ParseForm(); err != nil { + t.Fatalf("ParseForm() error = %v", err) + } + if got := r.PostForm.Get("veid"); got != "" { + t.Fatalf("post form veid = %q, want empty", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"error":0}`)) + })) + defer server.Close() + + c := NewClient("valid_key", "123456") + c.SetBaseURL(server.URL) + + if _, err := c.GetRateLimitStatus(context.Background()); err != nil { + t.Fatalf("GetRateLimitStatus() error = %v", err) + } +} + func TestClient_WriteAbuseMethods_Mock(t *testing.T) { tests := []struct { name string @@ -36,13 +304,7 @@ func TestClient_WriteAbuseMethods_Mock(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path[1:] - if path != tt.endpoint { - t.Fatalf("endpoint = %s, want %s", path, tt.endpoint) - } - if got := r.URL.Query().Get("record_id"); got != tt.wantRecord { - t.Fatalf("record_id = %q, want %q", got, tt.wantRecord) - } + assertPostForm(t, r, tt.endpoint, "valid_key", map[string]string{"record_id": tt.wantRecord}) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"error":0}`)) })) @@ -83,13 +345,10 @@ func TestClient_WriteMethods_InvalidInput(t *testing.T) { func TestClient_SetNotificationPreferences_Mock(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path[1:] - if path != "kiwivm/setNotificationPreferences" { - t.Fatalf("endpoint = %s, want kiwivm/setNotificationPreferences", path) - } + assertPostForm(t, r, "kiwivm/setNotificationPreferences", "valid_key", nil) var sent map[string]int - if err := json.Unmarshal([]byte(r.URL.Query().Get("json_notification_preferences")), &sent); err != nil { + if err := json.Unmarshal([]byte(r.PostForm.Get("json_notification_preferences")), &sent); err != nil { t.Fatalf("json_notification_preferences decode error = %v", err) } if sent["bandwidth-usage-alert-80"] != 1 { @@ -194,10 +453,7 @@ func TestClient_NewWriteMethods_BWHError(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path[1:] - if path != tt.endpoint { - t.Fatalf("endpoint = %s, want %s", path, tt.endpoint) - } + assertPostForm(t, r, tt.endpoint, "invalid_key", nil) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"error":700005,"message":"Authentication failure"}`)) })) @@ -216,3 +472,34 @@ func TestClient_NewWriteMethods_BWHError(t *testing.T) { }) } } + +func assertPostForm(t *testing.T, r *http.Request, endpoint, apiKey string, want map[string]string) { + t.Helper() + + if r.Method != http.MethodPost { + t.Fatalf("method = %s, want POST", r.Method) + } + if path := r.URL.Path[1:]; path != endpoint { + t.Fatalf("endpoint = %s, want %s", path, endpoint) + } + if r.URL.RawQuery != "" { + t.Fatalf("raw query = %q, want empty", r.URL.RawQuery) + } + if contentType := r.Header.Get("Content-Type"); !strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { + t.Fatalf("Content-Type = %q, want application/x-www-form-urlencoded", contentType) + } + if err := r.ParseForm(); err != nil { + t.Fatalf("ParseForm() error = %v", err) + } + if got := r.PostForm.Get("veid"); got != "123456" { + t.Fatalf("post form veid = %q, want 123456", got) + } + if got := r.PostForm.Get("api_key"); got != apiKey { + t.Fatalf("post form api_key = %q, want %s", got, apiKey) + } + for key, wantValue := range want { + if got := r.PostForm.Get(key); got != wantValue { + t.Fatalf("post form %s = %q, want %q", key, got, wantValue) + } + } +}