diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc6a0e..1903138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +### Added: +* Added `RedisVersion` to Fixed (Essentials) databases. + +### Changed: +* Added tests to strengthen coverage of fixed databases. +* Refactored structure of unit tests for fixed databases. + ## 0.41.0 (3rd November 2025) ### Added: diff --git a/README.md b/README.md index e9a97b7..96fb2bc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # rediscloud-go-api +[![CI](https://github.com/RedisLabs/rediscloud-go-api/workflows/CI/badge.svg)](https://github.com/RedisLabs/rediscloud-go-api/actions?query=workflow%3ACI) +[![Vulnerability Check](https://github.com/RedisLabs/rediscloud-go-api/workflows/Vulnerability%20Check/badge.svg)](https://github.com/RedisLabs/rediscloud-go-api/actions?query=workflow%3A%22Vulnerability+Check%22) + This repository is a Go SDK for the [Redis Cloud REST API](https://docs.redislabs.com/latest/rc/api/). ## Getting Started diff --git a/fixed_database_test.go b/fixed_database_test.go index 1597aaf..91c0985 100644 --- a/fixed_database_test.go +++ b/fixed_database_test.go @@ -96,6 +96,89 @@ func TestFixedDatabase_Create(t *testing.T) { assert.Equal(t, 51055029, actual) } +func TestFixedDatabase_Create_with_RedisVersion(t *testing.T) { + server := httptest.NewServer( + testServer( + "apiKey", + "secret", + postRequest( + t, + "/fixed/subscriptions/111728/databases", + `{ + "name": "my-redis-essentials-db", + "protocol": "redis", + "redisVersion": "7.4", + "dataPersistence": "none", + "dataEvictionPolicy": "noeviction", + "replication": false, + "alerts": [] + }`, + `{ + "taskId": "784299af-17ea-4ed6-b08f-dd643238c8dd", + "commandType": "fixedDatabaseCreateRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2024-05-10T14:14:14.736763484Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/784299af-17ea-4ed6-b08f-dd643238c8dd" + } + ] + }`, + ), + getRequest( + t, + "/tasks/784299af-17ea-4ed6-b08f-dd643238c8dd", + `{ + "taskId": "784299af-17ea-4ed6-b08f-dd643238c8dd", + "commandType": "fixedDatabaseCreateRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2024-05-10T14:14:34.153537279Z", + "response": { + "resourceId": 51055030, + "additionalResourceId": 111728 + }, + "links": [ + { + "rel": "resource", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/fixed/subscriptions/111728/databases/51055030" + }, + { + "rel": "self", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/784299af-17ea-4ed6-b08f-dd643238c8dd" + } + ] + }`, + ), + ), + ) + + subject, err := clientFromTestServer(server, "apiKey", "secret") + require.NoError(t, err) + + actual, err := subject.FixedDatabases.Create( + context.TODO(), + 111728, + fixedDatabases.CreateFixedDatabase{ + Name: redis.String("my-redis-essentials-db"), + Protocol: redis.String("redis"), + RedisVersion: redis.String("7.4"), + DataPersistence: redis.String("none"), + DataEvictionPolicy: redis.String("noeviction"), + Replication: redis.Bool(false), + Alerts: &[]*databases.Alert{}, + }, + ) + + require.NoError(t, err) + assert.Equal(t, 51055030, actual) +} + func TestFixedDatabase_List(t *testing.T) { server := httptest.NewServer( testServer( @@ -546,3 +629,331 @@ func TestFixedDatabase_Delete(t *testing.T) { require.NoError(t, err) } + +func TestFixedDatabase_UpgradeRedisVersion(t *testing.T) { + server := httptest.NewServer( + testServer( + "apiKey", + "secret", + postRequest( + t, + "/fixed/subscriptions/112119/databases/51056892/upgrade", + `{ "targetRedisVersion": "7.4" }`, + `{ + "taskId": "upgrade-task-uuid", + "commandType": "fixedDatabaseUpgradeRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2024-05-15T14:55:04.008723915Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/upgrade-task-uuid" + } + ] + }`, + ), + getRequest( + t, + "/tasks/upgrade-task-uuid", + `{ + "taskId": "upgrade-task-uuid", + "commandType": "fixedDatabaseUpgradeRequest", + "status": "processing-completed", + "description": "Request processing completed successfully and its resources are now being provisioned / de-provisioned.", + "timestamp": "2024-05-15T14:55:06.538386979Z", + "response": { + "resourceId": 51056892, + "additionalResourceId": 112119 + }, + "links": [ + { + "rel": "self", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/upgrade-task-uuid" + } + ] + }`, + ), + ), + ) + + subject, err := clientFromTestServer(server, "apiKey", "secret") + require.NoError(t, err) + + err = subject.FixedDatabases.UpgradeRedisVersion( + context.TODO(), + 112119, + 51056892, + fixedDatabases.UpgradeRedisVersion{ + TargetRedisVersion: redis.String("7.4"), + }, + ) + + require.NoError(t, err) +} + +// Helper functions for test setup + +func setupTestServer(t *testing.T, endpoints ...endpointRequest) *httptest.Server { + return httptest.NewServer(testServer("apiKey", "secret", endpoints...)) +} + +func testClient(t *testing.T, server *httptest.Server) *Client { + client, err := clientFromTestServer(server, "apiKey", "secret") + require.NoError(t, err) + return client +} + +// Error path tests - table-driven for similar scenarios + +func TestFixedDatabase_APIErrors(t *testing.T) { + tests := []struct { + name string + path string + httpMethod string + requestBody string + statusCode int + errorBody string + apiCall func(*Client) error + }{ + { + name: "Create_APIError", + path: "/fixed/subscriptions/111728/databases", + httpMethod: "POST", + requestBody: `{"name":"my-test-fixed-database","protocol":"memcached","respVersion":"resp2","dataPersistence":"none","dataEvictionPolicy":"noeviction","replication":false,"alerts":[]}`, + statusCode: 400, + errorBody: `{"errorCode": "INVALID_REQUEST"}`, + apiCall: func(c *Client) error { + _, err := c.FixedDatabases.Create(context.TODO(), 111728, + fixedDatabases.CreateFixedDatabase{ + Name: redis.String("my-test-fixed-database"), + Protocol: redis.String("memcached"), + RespVersion: redis.String("resp2"), + DataPersistence: redis.String("none"), + DataEvictionPolicy: redis.String("noeviction"), + Replication: redis.Bool(false), + Alerts: &[]*databases.Alert{}, + }) + return err + }, + }, + { + name: "Update_APIError", + path: "/fixed/subscriptions/112119/databases/51056892", + httpMethod: "PUT", + requestBody: `{"name":"my-test-fixed-database","respVersion":"resp2","dataPersistence":"none","dataEvictionPolicy":"volatile-lru","replication":false,"enableDefaultUser":true,"alerts":[{"name":"datasets-size","value":80}]}`, + statusCode: 404, + errorBody: `{"errorCode": "DATABASE_NOT_FOUND"}`, + apiCall: func(c *Client) error { + return c.FixedDatabases.Update(context.TODO(), 112119, 51056892, + fixedDatabases.UpdateFixedDatabase{ + Name: redis.String("my-test-fixed-database"), + RespVersion: redis.String("resp2"), + DataPersistence: redis.String("none"), + DataEvictionPolicy: redis.String("volatile-lru"), + Replication: redis.Bool(false), + EnableDefaultUser: redis.Bool(true), + Alerts: &[]*databases.Alert{ + { + Name: redis.String("datasets-size"), + Value: redis.Int(80), + }, + }, + }) + }, + }, + { + name: "Delete_APIError", + path: "/fixed/subscriptions/112119/databases/51056892", + httpMethod: "DELETE", + statusCode: 500, + errorBody: `{"errorCode": "INTERNAL_ERROR"}`, + apiCall: func(c *Client) error { + return c.FixedDatabases.Delete(context.TODO(), 112119, 51056892) + }, + }, + { + name: "UpgradeRedisVersion_APIError", + path: "/fixed/subscriptions/112119/databases/51056892/upgrade", + httpMethod: "POST", + requestBody: `{"targetRedisVersion":"7.4"}`, + statusCode: 400, + errorBody: `{"errorCode": "INVALID_REDIS_VERSION"}`, + apiCall: func(c *Client) error { + return c.FixedDatabases.UpgradeRedisVersion(context.TODO(), 112119, 51056892, + fixedDatabases.UpgradeRedisVersion{ + TargetRedisVersion: redis.String("7.4"), + }) + }, + }, + { + name: "Backup_APIError", + path: "/fixed/subscriptions/112119/databases/51056892/backup", + httpMethod: "POST", + statusCode: 500, + errorBody: `{"errorCode": "BACKUP_FAILED"}`, + apiCall: func(c *Client) error { + return c.FixedDatabases.Backup(context.TODO(), 112119, 51056892) + }, + }, + { + name: "Import_APIError", + path: "/fixed/subscriptions/112119/databases/51056892/import", + httpMethod: "POST", + requestBody: `{"sourceType":"rdb-file","importFromUri":["s3://bucket/file.rdb"]}`, + statusCode: 400, + errorBody: `{"errorCode": "INVALID_SOURCE"}`, + apiCall: func(c *Client) error { + return c.FixedDatabases.Import(context.TODO(), 112119, 51056892, + fixedDatabases.Import{ + SourceType: redis.String("rdb-file"), + ImportFromURI: redis.StringSlice("s3://bucket/file.rdb"), + }) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var endpoint endpointRequest + switch tt.httpMethod { + case "POST": + if tt.requestBody != "" { + endpoint = postRequestWithStatus(t, tt.path, tt.requestBody, tt.statusCode, tt.errorBody) + } else { + endpoint = postRequestWithNoRequestAndStatus(t, tt.path, tt.statusCode, tt.errorBody) + } + case "PUT": + endpoint = putRequestWithStatus(t, tt.path, tt.requestBody, tt.statusCode, tt.errorBody) + case "DELETE": + endpoint = deleteRequestWithStatus(t, tt.path, tt.statusCode, tt.errorBody) + } + + server := setupTestServer(t, endpoint) + defer server.Close() + + client := testClient(t, server) + err := tt.apiCall(client) + + require.Error(t, err) + }) + } +} + +func TestFixedDatabase_UpgradeRedisVersion_TaskWaiterError(t *testing.T) { + server := httptest.NewServer( + testServer( + "apiKey", + "secret", + postRequest( + t, + "/fixed/subscriptions/112119/databases/51056892/upgrade", + `{ "targetRedisVersion": "7.4" }`, + `{ + "taskId": "upgrade-task-uuid", + "commandType": "fixedDatabaseUpgradeRequest", + "status": "received", + "description": "Task request received and is being queued for processing.", + "timestamp": "2024-05-15T14:55:04.008723915Z", + "links": [ + { + "rel": "task", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/upgrade-task-uuid" + } + ] + }`, + ), + getRequest( + t, + "/tasks/upgrade-task-uuid", + `{ + "taskId": "upgrade-task-uuid", + "commandType": "fixedDatabaseUpgradeRequest", + "status": "processing-failed", + "description": "Task processing failed - database version is already at target version", + "timestamp": "2024-05-15T14:55:06.538386979Z", + "response": {}, + "links": [ + { + "rel": "self", + "type": "GET", + "href": "https://api-staging.qa.redislabs.com/v1/tasks/upgrade-task-uuid" + } + ] + }`, + ), + ), + ) + + subject, err := clientFromTestServer(server, "apiKey", "secret") + require.NoError(t, err) + + err = subject.FixedDatabases.UpgradeRedisVersion( + context.TODO(), + 112119, + 51056892, + fixedDatabases.UpgradeRedisVersion{ + TargetRedisVersion: redis.String("7.4"), + }, + ) + + require.Error(t, err) +} + +func TestFixedDatabase_Backup_APIError(t *testing.T) { + // Test API returning error during backup + server := httptest.NewServer( + testServer("apiKey", "secret", + postRequestWithNoRequestAndStatus( + t, + "/fixed/subscriptions/112119/databases/51056892/backup", + 500, + `{"errorCode": "BACKUP_FAILED"}`, + ), + ), + ) + defer server.Close() + + subject, err := clientFromTestServer(server, "apiKey", "secret") + require.NoError(t, err) + + err = subject.FixedDatabases.Backup(context.TODO(), 112119, 51056892) + require.Error(t, err) +} + +func TestFixedDatabase_Import_APIError(t *testing.T) { + // Test API returning error during import + server := httptest.NewServer( + testServer("apiKey", "secret", + postRequestWithStatus( + t, + "/fixed/subscriptions/112119/databases/51056892/import", + `{ + "sourceType": "rdb-file", + "importFromUri": ["s3://bucket/file.rdb"] + }`, + 400, + `{"errorCode": "INVALID_SOURCE"}`, + ), + ), + ) + defer server.Close() + + subject, err := clientFromTestServer(server, "apiKey", "secret") + require.NoError(t, err) + + err = subject.FixedDatabases.Import( + context.TODO(), + 112119, + 51056892, + fixedDatabases.Import{ + SourceType: redis.String("rdb-file"), + ImportFromURI: redis.StringSlice("s3://bucket/file.rdb"), + }, + ) + + require.Error(t, err) +} diff --git a/service/fixed/databases/model.go b/service/fixed/databases/model.go index 510e80c..de84fd3 100644 --- a/service/fixed/databases/model.go +++ b/service/fixed/databases/model.go @@ -11,6 +11,7 @@ import ( type CreateFixedDatabase struct { Name *string `json:"name,omitempty"` Protocol *string `json:"protocol,omitempty"` + RedisVersion *string `json:"redisVersion,omitempty"` MemoryLimitInGB *float64 `json:"memoryLimitInGb,omitempty"` DatasetSizeInGB *float64 `json:"datasetSizeInGb,omitempty"` SupportOSSClusterAPI *bool `json:"supportOSSClusterApi,omitempty"` @@ -60,6 +61,7 @@ type FixedDatabase struct { Protocol *string `json:"protocol,omitempty"` Provider *string `json:"provider,omitempty"` Region *string `json:"region,omitempty"` + RedisVersion *string `json:"redisVersion,omitempty"` RedisVersionCompliance *string `json:"redisVersionCompliance,omitempty"` RespVersion *string `json:"respVersion,omitempty"` Status *string `json:"status,omitempty"` @@ -186,6 +188,10 @@ func (f *NotFound) Error() string { return fmt.Sprintf("fixed database %d in subscription %d not found", f.dbId, f.subId) } +type UpgradeRedisVersion struct { + TargetRedisVersion *string `json:"targetRedisVersion,omitempty"` +} + func ProtocolValues() []string { return []string{ "redis", diff --git a/service/fixed/databases/service.go b/service/fixed/databases/service.go index 1c4ccaf..f3342df 100644 --- a/service/fixed/databases/service.go +++ b/service/fixed/databases/service.go @@ -124,6 +124,19 @@ func (a *API) Import(ctx context.Context, subscription int, database int, reques return a.taskWaiter.Wait(ctx, *task.ID) } +// UpgradeRedisVersion will upgrade the Redis version of an existing fixed database. +func (a *API) UpgradeRedisVersion(ctx context.Context, subscription int, database int, upgradeVersion UpgradeRedisVersion) error { + var task internal.TaskResponse + err := a.client.Post(ctx, fmt.Sprintf("upgrade fixed database %d version for subscription %d", database, subscription), fmt.Sprintf("/fixed/subscriptions/%d/databases/%d/upgrade", subscription, database), upgradeVersion, &task) + if err != nil { + return err + } + + a.logger.Printf("Waiting for fixed database %d for subscription %d to finish being upgraded", database, subscription) + + return a.taskWaiter.Wait(ctx, *task.ID) +} + type ListFixedDatabase struct { client HttpClient subscription int diff --git a/util_test.go b/util_test.go index 817a761..f4d1ebd 100644 --- a/util_test.go +++ b/util_test.go @@ -165,6 +165,17 @@ func postRequest(t *testing.T, path string, request string, body string) endpoin } } +func postRequestWithStatus(t *testing.T, path string, request string, status int, body string) endpointRequest { + return endpointRequest{ + method: http.MethodPost, + path: path, + body: body, + requestBody: &request, + status: status, + t: t, + } +} + func postRequestWithNoRequest(t *testing.T, path string, body string) endpointRequest { return endpointRequest{ method: http.MethodPost, @@ -175,6 +186,16 @@ func postRequestWithNoRequest(t *testing.T, path string, body string) endpointRe } } +func postRequestWithNoRequestAndStatus(t *testing.T, path string, status int, body string) endpointRequest { + return endpointRequest{ + method: http.MethodPost, + path: path, + body: body, + status: status, + t: t, + } +} + func putRequest(t *testing.T, path string, request string, body string) endpointRequest { return endpointRequest{ method: http.MethodPut, @@ -186,6 +207,17 @@ func putRequest(t *testing.T, path string, request string, body string) endpoint } } +func putRequestWithStatus(t *testing.T, path string, request string, status int, body string) endpointRequest { + return endpointRequest{ + method: http.MethodPut, + path: path, + body: body, + requestBody: &request, + status: status, + t: t, + } +} + // taskFlow returns the two endpointRequests needed for a "POST/PUT/DELETE -> GET /tasks/{id}" flow func taskFlow(t *testing.T, method, path, requestBody, taskID, commandType string) []endpointRequest { now := time.Now().UTC().Format(time.RFC3339) // e.g. "2025-08-11T14:33:21Z"