From 5762fe33aa25ba8094803667b388582e68b45d15 Mon Sep 17 00:00:00 2001 From: AsifNawaz-cnic Date: Tue, 13 Aug 2024 14:14:24 +0000 Subject: [PATCH] feat(centralnic-reseller-go-sdk): Introducing CentralNic Reseller Go-SDK API Connector BREAKING CHANGE: This release deprecates the Hexonet Go SDK and introduces the CentralNic Reseller Go SDK. - Note: To continue using the Hexonet SDK, please install version 4.0.7 or earlier. --- .../linters/.golangci.yml | 17 +- README.md | 36 +- apiclient.go | 9 - apiclient/apiclient.go | 282 +++++++++------- apiclient/apiclient_test.go | 319 +++++++----------- column/column.go | 5 +- customlogger/customlogger.go | 4 +- demo/demo.go | 53 +++ go.mod | 2 +- idntranslator/idntranslator.go | 6 +- idntranslator/idntranslator_test.go | 8 +- logger/logger.go | 2 +- package.json | 19 +- record/record.go | 3 + response/response.go | 107 ++++-- response/response_test.go | 64 ++-- responseparser/responseparser.go | 44 --- responseparser/responseparser_test.go | 51 --- .../responsetemplatemanager.go | 2 +- .../responsetemplatemanager_test.go | 6 +- responsetranslator/responsetranslator.go | 62 ++-- responsetranslator/responsetranslator_test.go | 34 +- socketconfig/socketconfig.go | 73 +--- 23 files changed, 591 insertions(+), 617 deletions(-) rename .golangci.yml => .github/linters/.golangci.yml (56%) delete mode 100644 apiclient.go create mode 100644 demo/demo.go delete mode 100644 responseparser/responseparser_test.go diff --git a/.golangci.yml b/.github/linters/.golangci.yml similarity index 56% rename from .golangci.yml rename to .github/linters/.golangci.yml index 47120ae..dd601ea 100644 --- a/.golangci.yml +++ b/.github/linters/.golangci.yml @@ -5,8 +5,8 @@ ######################### ######################### -# https://raw.githubusercontent.com/super-linter/super-linter/main/.github/linters/.golangci.yml - +# configure golangci-lint +# see https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml issues: exclude-rules: - path: _test\.go @@ -22,13 +22,20 @@ linters: - goconst - goimports - gocritic + - govet - revive - - shadow # Added shadow linter linters-settings: errcheck: - # report about assignment of errors to blank identifier + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; # default is false: such cases aren't reported by default. check-blank: true + govet: + enable: + # report about shadowed variables + - shadowing gocyclo: # minimal code complexity to report, 30 by default - min-complexity: 15 + min-complexity: 20 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true \ No newline at end of file diff --git a/README.md b/README.md index 0ba7601..dec0499 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,41 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/blob/master/CONTRIBUTING.md) -This module is a connector library for the insanely fast HEXONET Backend API. For further informations visit our [homepage](http://hexonet.net) and do not hesitate to [contact us](https://www.hexonet.net/contact). +This module is a connector library for the insanely fast CentralNic Reseller Backend API. For further informations visit our [homepage](https://www.centralnicreseller.com) and do not hesitate to [contact us](https://www.centralnicreseller.com/contact). + +## Deprecation Notice: Hexonet Go SDK + +This SDK succeeds the deprecated Hexonet Go SDK. It is an enhanced version that builds upon the foundation laid by the Hexonet SDK, offering improved features and performance. Hexonet is migrating to CentralNic Reseller, ensuring continued support and development under the new branding. ## Resources -- Documentation: - - [HEXONET](https://www.hexonet.support/hc/en-gb/articles/13651860201117-Self-Development-Kit-for-Go-Golang) -- [Release Notes](https://github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/releases) +- [Documentation](https://support.centralnicreseller.com/hc/en-gb/articles/5714403954333-Self-Development-Kit-for-Go-Golang) + +## Release Notes + +For detailed release notes, please visit the [Release Notes](https://github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/releases) page. + +## Running the Demo Application + +To run the demo application, follow these steps: + +1. **Set Your Credentials**: Ensure your credentials are available. You can either: + - Replace them directly in the application file. + - Set the environment variables `CNR_TEST_USER` and `CNR_TEST_PASSWORD` in your terminal. + +2. **Execute the Demo**: Once your credentials are set, run the following command in the terminal: + + ```sh + npm run test-demo + ``` + +3. **Update Demo Contents**: If you need to update the contents of the demo file, you can find it at: + + ```plaintext + demo/demo.go + ``` + +By following these steps, you can successfully run and update the demo application. ## Authors diff --git a/apiclient.go b/apiclient.go deleted file mode 100644 index ea32795..0000000 --- a/apiclient.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) 2018 Kai Schwarz (HEXONET GmbH). All rights reserved. -// -// Use of this source code is governed by the MIT -// license that can be found in the LICENSE.md file. - -package main - -func main() { -} diff --git a/apiclient/apiclient.go b/apiclient/apiclient.go index 8a8e57f..a0a9615 100644 --- a/apiclient/apiclient.go +++ b/apiclient/apiclient.go @@ -1,3 +1,46 @@ +// Package apiclient provides a client for communicating with the HEXONET backend API. +// +// This package allows two types of communication: +// - Session-based communication: Used for building custom frontends and supports 2FA (Two-Factor Authentication). +// - Sessionless communication: Used for simple command requests. +// +// The package includes the following features: +// - Connection setup: The package supports three connection setups: high performance, default, and OT&E (demo system). +// - API request: The package provides methods for making API requests and handling responses. +// - Session management: The package allows for session login, logout, and session reuse. +// - Debug mode: The package supports enabling and disabling debug mode for logging and output. +// - Proxy configuration: The package allows for setting and retrieving proxy configurations for API communication. +// - User agent customization: The package provides methods for customizing the user agent header. +// - Command parameter handling: The package includes methods for flattening command parameters and automatically converting IDN (Internationalized Domain Name) values to punycode. +// - Pagination support: The package includes methods for requesting next response pages and retrieving all response pages for a given query. +// +// For more information on the available commands, refer to the HEXONET API documentation: https://github.com/hexonet/hexonet-api-documentation/tree/master/API +// +// Example usage: +// // Create a new APIClient instance +// client := apiclient.NewAPIClient() +// +// // Set credentials for API communication +// client.SetCredentials("username", "password") +// +// // Make an API request +// response := client.Request(map[string]interface{}{ +// "COMMAND": "StatusAccount", +// }) +// +// // Check if the request was successful +// if response.IsSuccess() { +// // Process the response data +// // ... +// } else { +// // Handle the error +// // ... +// } +// +// // Close the API session +// client.Logout() +// +// Note: This package is based on the HEXONET API documentation and is subject to change. Please refer to the documentation for the most up-to-date information. // Copyright (c) 2018 Kai Schwarz (HEXONET GmbH). All rights reserved. // // Use of this source code is governed by the MIT @@ -20,21 +63,21 @@ import ( "strings" "time" - IDN "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/idntranslator" - LG "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/logger" - R "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/response" - RTM "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responsetemplatemanager" - SC "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/socketconfig" + IDN "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/idntranslator" + LG "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/logger" + R "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/response" + RTM "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/responsetemplatemanager" + SC "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/socketconfig" ) -// ISPAPI_CONNECTION_URL_PROXY represents the url used for the high performance connection setup -const ISPAPI_CONNECTION_URL_PROXY = "http://127.0.0.1/api/call.cgi" //nolint +// CNR_CONNECTION_URL_PROXY represents the url used for the high performance connection setup +const CNR_CONNECTION_URL_PROXY = "http://127.0.0.1/api/call.cgi" //nolint -// ISPAPI_CONNECTION_URL_LIVE represents the url used for the default connection setup -const ISPAPI_CONNECTION_URL_LIVE = "https://api.ispapi.net/api/call.cgi" //nolint +// CNR_CONNECTION_URL_LIVE represents the url used for the default connection setup +const CNR_CONNECTION_URL_LIVE = "https://api.rrpproxy.net/api/call.cgi" //nolint -// ISPAPI_CONNECTION_URL_OTE represents the url used for the OT&E (demo system) connection setup -const ISPAPI_CONNECTION_URL_OTE = "https://api-ote.ispapi.net/api/call.cgi" //nolint +// CNR_CONNECTION_URL_OTE represents the url used for the OT&E (demo system) connection setup +const CNR_CONNECTION_URL_OTE = "https://api-ote.rrpproxy.net/api/call.cgi" //nolint var rtm = RTM.GetInstance() @@ -59,6 +102,20 @@ type APIClient struct { curlopts map[string]string ua string logger LG.ILogger + subUser string + roleSeparator string +} + +// RequestOptions represents the options for an API request. +type RequestOptions struct { + SetUserView bool // SetUserView indicates whether to set a data view to a given subuser. +} + +// NewRequestOptions creates a new instance of RequestOptions with default values. +func NewRequestOptions() *RequestOptions { + return &RequestOptions{ + SetUserView: true, + } } // NewAPIClient represents the constructor for struct APIClient. @@ -66,11 +123,12 @@ func NewAPIClient() *APIClient { cl := &APIClient{ debugMode: false, socketTimeout: 300 * time.Second, - socketURL: ISPAPI_CONNECTION_URL_LIVE, + socketURL: CNR_CONNECTION_URL_LIVE, socketConfig: SC.NewSocketConfig(), curlopts: map[string]string{}, ua: "", logger: nil, + roleSeparator: ":", } cl.UseLIVESystem() cl.SetDefaultLogger() @@ -105,7 +163,7 @@ func (cl *APIClient) GetProxy() (string, error) { if exists { return val, nil } - return "", errors.New("No proxy configuration available") + return "", errors.New("no proxy configuration available") } // SetReferer method to set a value for HTTP Header `Referer` to use for API communication @@ -124,7 +182,7 @@ func (cl *APIClient) GetReferer() (string, error) { if exists { return val, nil } - return "", errors.New("No configuration available for HTTP Header `Referer`") + return "", errors.New("no configuration available for HTTP Header `Referer`") } // EnableDebugMode method to enable Debug Output to logger @@ -139,6 +197,24 @@ func (cl *APIClient) DisableDebugMode() *APIClient { return cl } +// SetUserView method to set a data view to a given subuser +func (cl *APIClient) SetUserView(uid string) *APIClient { + cl.subUser = uid + return cl +} + +// ResetUserView method to reset data view back from subuser to user +func (cl *APIClient) ResetUserView() *APIClient { + cl.subUser = "" + return cl +} + +// UseHighPerformanceConnectionSetup to activate high performance conneciton setup +func (cl *APIClient) UseHighPerformanceConnectionSetup() *APIClient { + cl.SetURL(CNR_CONNECTION_URL_PROXY) + return cl +} + // GetPOSTData method to Serialize given command for POST request // including connection configuration data func (cl *APIClient) GetPOSTData(cmd map[string]string, secured ...bool) string { @@ -167,7 +243,10 @@ func (cl *APIClient) GetPOSTData(cmd map[string]string, secured ...bool) string re := regexp.MustCompile("PASSWORD=[^\n]+") str = re.ReplaceAllString(str, "PASSWORD=***") } - str = str[:len(str)-1] // Remove \n at end + if tmp.String() == "" { + return strings.TrimSuffix(data, "&") + } + str = strings.TrimSuffix(str, "\n") return strings.Join([]string{ data, url.QueryEscape("s_command"), @@ -176,15 +255,6 @@ func (cl *APIClient) GetPOSTData(cmd map[string]string, secured ...bool) string }, "") } -// GetSession method to get the API Session that is currently set -func (cl *APIClient) GetSession() (string, error) { - sessid := cl.socketConfig.GetSession() - if len(sessid) == 0 { - return "", errors.New("Could not find an active session") - } - return sessid, nil -} - // GetURL method to get the API connection url that is currently set func (cl *APIClient) GetURL() string { return cl.socketURL @@ -219,8 +289,8 @@ func (cl *APIClient) GetVersion() string { // Please save/update that map into user session func (cl *APIClient) SaveSession(sessionobj map[string]interface{}) *APIClient { sessionobj["socketcfg"] = map[string]string{ - "entity": cl.socketConfig.GetSystemEntity(), "session": cl.socketConfig.GetSession(), + "login": cl.socketConfig.GetLogin(), } return cl } @@ -228,9 +298,15 @@ func (cl *APIClient) SaveSession(sessionobj map[string]interface{}) *APIClient { // ReuseSession method to reuse given configuration out of a user session // to rebuild and reuse connection settings func (cl *APIClient) ReuseSession(sessionobj map[string]interface{}) *APIClient { - cfg := sessionobj["socketcfg"].(map[string]string) - cl.socketConfig.SetSystemEntity(cfg["entity"]) - cl.SetSession(cfg["session"]) + if sessionobj == nil || sessionobj["socketcfg"] == nil { + return cl + } + cfg, ok := sessionobj["socketcfg"].(map[string]string) + if !ok || cfg["login"] == "" || cfg["session"] == "" { + return cl + } + cl.SetCredentials(cfg["login"]) + cl.socketConfig.SetSession(cfg["session"]) return cl } @@ -240,85 +316,50 @@ func (cl *APIClient) SetURL(value string) *APIClient { return cl } -// SetOTP method to set one time password to be used for API communication -func (cl *APIClient) SetOTP(value string) *APIClient { - cl.socketConfig.SetOTP(value) - return cl -} - -// SetSession method to set an API session id to be used for API communication -func (cl *APIClient) SetSession(value string) *APIClient { - cl.socketConfig.SetSession(value) - return cl -} - -// SetRemoteIPAddress method to set an Remote IP Address to be used for API communication -func (cl *APIClient) SetRemoteIPAddress(value string) *APIClient { - cl.socketConfig.SetRemoteAddress(value) +// SetPersistent method sets the API connection to use a persistent session +func (cl *APIClient) SetPersistent() *APIClient { + cl.socketConfig.SetPersistent() return cl } // SetCredentials method to set Credentials to be used for API communication -func (cl *APIClient) SetCredentials(uid string, pw string) *APIClient { - cl.socketConfig.SetLogin(uid) - cl.socketConfig.SetPassword(pw) +func (cl *APIClient) SetCredentials(params ...string) *APIClient { + if len(params) > 0 { + cl.socketConfig.SetLogin(params[0]) + } + if len(params) > 1 { + cl.socketConfig.SetPassword(params[1]) + } return cl } // SetRoleCredentials method to set Role User Credentials to be used for API communication -func (cl *APIClient) SetRoleCredentials(uid string, role string, pw string) *APIClient { - if len(role) > 0 { - return cl.SetCredentials(uid+"!"+role, pw) - } - return cl.SetCredentials(uid, pw) -} - -// Login method to perform API login to start session-based communication -// 1st parameter: one time password -func (cl *APIClient) Login(params ...string) *R.Response { - otp := "" +func (cl *APIClient) SetRoleCredentials(params ...string) *APIClient { if len(params) > 0 { - otp = params[0] - } - cl.SetOTP(otp) - rr := cl.Request(map[string]interface{}{"COMMAND": "StartSession"}) - if rr.IsSuccess() { - col := rr.GetColumn("SESSION") - if col != nil { - cl.SetSession(col.GetData()[0]) - } else { - cl.SetSession("") + uid := params[0] + if len(params) > 1 && len(params[1]) > 0 { + role := params[1] + uid = uid + cl.roleSeparator + role + } + if len(params) > 2 { + pw := params[2] + return cl.SetCredentials(uid, pw) } + return cl.SetCredentials(uid) } - return rr + return cl } -// LoginExtended method to perform API login to start session-based communication. -// 1st parameter: map of additional command parameters -// 2nd parameter: one time password -func (cl *APIClient) LoginExtended(params ...interface{}) *R.Response { - otp := "" - parameters := map[string]string{} - if len(params) == 2 { - otp = params[1].(string) - } - cl.SetOTP(otp) - if len(params) > 0 { - parameters = params[0].(map[string]string) - } - cmd := map[string]interface{}{ - "COMMAND": "StartSession", - } - for k, v := range parameters { - cmd[k] = v - } - rr := cl.Request(cmd) +// Login method to perform API login to start session-based communication +// 1st parameter: one time password +func (cl *APIClient) Login() *R.Response { + cl.SetPersistent() + rr := cl.Request(make(map[string]interface{}), &RequestOptions{SetUserView: false}) + cl.socketConfig.SetSession("") if rr.IsSuccess() { - col := rr.GetColumn("SESSION") + col := rr.GetColumn("SESSIONID") if col != nil { - cl.SetSession(col.GetData()[0]) - } else { - cl.SetSession("") + cl.socketConfig.SetSession(col.GetData()[0]) } } return rr @@ -327,16 +368,27 @@ func (cl *APIClient) LoginExtended(params ...interface{}) *R.Response { // Logout method to perform API logout to close API session in use func (cl *APIClient) Logout() *R.Response { rr := cl.Request(map[string]interface{}{ - "COMMAND": "EndSession", - }) + "COMMAND": "StopSession", + }, &RequestOptions{SetUserView: false}) if rr.IsSuccess() { - cl.SetSession("") + cl.socketConfig.SetSession("") } return rr } // Request method to perform API request using the given command -func (cl *APIClient) Request(cmd map[string]interface{}) *R.Response { +func (cl *APIClient) Request(cmd map[string]interface{}, opts ...*RequestOptions) *R.Response { + // Use default RequestOptions if opts is not available + options := NewRequestOptions() + if len(opts) > 0 { + options = opts[0] + } + + // Check if SetUserView option is enabled and subUser is set + if (options.SetUserView) && (len(cl.subUser) > 0) { + cmd["SUBUSER"] = cl.subUser + } + // flatten nested api command bulk parameters newcmd := cl.flattenCommand(cmd) // auto convert umlaut names to punycode @@ -421,11 +473,11 @@ func (cl *APIClient) RequestNextResponsePage(rr *R.Response) (*R.Response, error mycmd[key] = val } if _, ok := mycmd["LAST"]; ok { - return nil, errors.New("Parameter LAST in use. Please remove it to avoid issues in requestNextPage") + return nil, errors.New("parameter LAST in use. Please remove it to avoid issues in requestNextPage") } first := 0 if v, ok := mycmd["FIRST"]; ok { - first, _ = fmt.Sscan("%s", v) + first, _ = fmt.Sscan("%s", v) //nolint:errcheck } total := rr.GetRecordsTotalCount() limit := rr.GetRecordsLimitation() @@ -435,7 +487,7 @@ func (cl *APIClient) RequestNextResponsePage(rr *R.Response) (*R.Response, error mycmd["LIMIT"] = fmt.Sprintf("%d", limit) return cl.Request(mycmd), nil } - return nil, errors.New("Could not find further existing pages") + return nil, errors.New("could not find further existing pages") } // RequestAllResponsePages method to request all pages/entries for the given query command @@ -460,48 +512,31 @@ func (cl *APIClient) RequestAllResponsePages(cmd map[string]string) []R.Response return responses } -// SetUserView method to set a data view to a given subuser -func (cl *APIClient) SetUserView(uid string) *APIClient { - cl.socketConfig.SetUser(uid) - return cl -} - -// ResetUserView method to reset data view back from subuser to user -func (cl *APIClient) ResetUserView() *APIClient { - cl.socketConfig.SetUser("") - return cl -} - -// UseHighPerformanceConnectionSetup to activate high performance conneciton setup -func (cl *APIClient) UseHighPerformanceConnectionSetup() *APIClient { - cl.SetURL(ISPAPI_CONNECTION_URL_PROXY) - return cl -} - // UseDefaultConnectionSetup to activate default conneciton setup (the default anyways) func (cl *APIClient) UseDefaultConnectionSetup() *APIClient { - cl.SetURL(ISPAPI_CONNECTION_URL_LIVE) + cl.SetURL(CNR_CONNECTION_URL_LIVE) return cl } // UseOTESystem method to set OT&E System for API communication func (cl *APIClient) UseOTESystem() *APIClient { - cl.SetURL(ISPAPI_CONNECTION_URL_OTE) - cl.socketConfig.SetSystemEntity("1234") + cl.SetURL(CNR_CONNECTION_URL_OTE) return cl } // UseLIVESystem method to set LIVE System for API communication // Usage of LIVE System is active by default. func (cl *APIClient) UseLIVESystem() *APIClient { - cl.SetURL(ISPAPI_CONNECTION_URL_LIVE) - cl.socketConfig.SetSystemEntity("54cd") + cl.SetURL(CNR_CONNECTION_URL_LIVE) return cl } // flattenCommand method to translate all command parameter names to uppercase func (cl *APIClient) flattenCommand(cmd map[string]interface{}) map[string]string { newcmd := map[string]string{} + if len(cmd) == 0 { + return newcmd + } for key, val := range cmd { newKey := strings.ToUpper(key) if reflect.TypeOf(val).Kind() == reflect.Slice { @@ -523,6 +558,9 @@ func (cl *APIClient) flattenCommand(cmd map[string]interface{}) map[string]strin // autoIDNConvert method to translate all whitelisted parameter values to punycode, if necessary func (cl *APIClient) autoIDNConvert(cmd map[string]string) map[string]string { + if len(cmd) == 0 { + return cmd + } // don't convert for convertidn command to avoid endless loop pattern := regexp.MustCompile(`(?i)^CONVERTIDN$`) mm := pattern.MatchString(cmd["COMMAND"]) diff --git a/apiclient/apiclient_test.go b/apiclient/apiclient_test.go index 0f1515b..cb37cd7 100644 --- a/apiclient/apiclient_test.go +++ b/apiclient/apiclient_test.go @@ -2,13 +2,15 @@ package apiclient import ( "fmt" + "net/url" "os" "runtime" "strconv" "strings" "testing" - R "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/response" + R "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/response" + "github.com/stretchr/testify/assert" ) var cl = NewAPIClient() @@ -17,11 +19,11 @@ func TestMain(m *testing.M) { rtm.AddTemplate( "login200", - "[RESPONSE]\r\nPROPERTY[SESSION][0]=h8JLZZHdF2WgWWXlwbKWzEG3XrzoW4yshhvtqyg0LCYiX55QnhgYX9cB0W4mlpbx\r\nDESCRIPTION=Command completed successfully\r\nCODE=200\r\nQUEUETIME=0\r\nRUNTIME=0.169\r\nEOF\r\n", + "[RESPONSE]\r\nproperty[expiration date][0] = 2024-09-19 10:52:51\r\nproperty[sessionid][0] = bb7a884b09b9a674fb4a22211758ce87\r\ndescription = Command completed successfully\r\ncode = 200\r\nqueuetime = 0.004\r\nruntime = 0.023\r\nEOF\r\n", ) rtm.AddTemplate( "listP0", - "[RESPONSE]\r\nPROPERTY[TOTAL][0]=2701\r\nPROPERTY[FIRST][0]=0\r\nPROPERTY[DOMAIN][0]=0-60motorcycletimes.com\r\nPROPERTY[DOMAIN][1]=0-be-s01-0.com\r\nPROPERTY[COUNT][0]=2\r\nPROPERTY[LAST][0]=1\r\nPROPERTY[LIMIT][0]=2\r\nDESCRIPTION=Command completed successfully\r\nCODE=200\r\nQUEUETIME=0\r\nRUNTIME=0.023\r\nEOF\r\n", + "[RESPONSE]\r\nproperty[total][0] = 4\r\nproperty[first][0] = 0\r\nproperty[domain][0] = cnic-ssl-test1.com\r\nproperty[domain][1] = cnic-ssl-test2.com\r\nproperty[count][0] = 2\r\nproperty[last][0] = 1\r\nproperty[limit][0] = 2\r\ndescription = Command completed successfully\r\ncode = 200\r\nqueuetime = 0\r\nruntime = 0.007\r\nEOF\r\n", ) rtm.AddTemplate( "OK", @@ -31,7 +33,7 @@ func TestMain(m *testing.M) { } func TestGetPOSTData1(t *testing.T) { - validate := "s_entity=54cd&s_command=AUTH%3Dgwrgwqg%25%26%5C44t3%2A%0ACOMMAND%3DModifyDomain" + validate := "s_command=AUTH%3Dgwrgwqg%25%26%5C44t3%2A%0ACOMMAND%3DModifyDomain" enc := cl.GetPOSTData(map[string]string{ "COMMAND": "ModifyDomain", "AUTH": "gwrgwqg%&\\44t3*", @@ -42,12 +44,13 @@ func TestGetPOSTData1(t *testing.T) { } func TestGetPOSTDataSecured(t *testing.T) { - validate := "s_entity=54cd&s_login=test.user&s_pw=***&s_command=COMMAND%3DCheckAuthentication%0APASSWORD%3D%2A%2A%2A%0ASUBUSER%3Dtest.user" - cl.SetCredentials("test.user", "test.passw0rd") + testUser := url.QueryEscape(os.Getenv("CNR_TEST_USER")) + validate := "s_login=" + testUser + "&s_pw=***&s_command=COMMAND%3DCheckAuthentication%0APASSWORD%3D%2A%2A%2A%0ASUBUSER%3D" + testUser + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) enc := cl.GetPOSTData(map[string]string{ "COMMAND": "CheckAuthentication", - "SUBUSER": "test.user", - "PASSWORD": "test.passw0rd", + "SUBUSER": os.Getenv("CNR_TEST_USER"), + "PASSWORD": os.Getenv("CNR_TEST_PASSWORD"), }, true) if strings.Compare(enc, validate) != 0 { t.Error(fmt.Printf("TestGetPOSTDataSecured: Expected encoding result not matching\n\n%s\n%s.", enc, validate)) @@ -64,7 +67,7 @@ func TestDisableDebugMode(_ *testing.T) { } func TestRequestFlattenCommand(t *testing.T) { - cl.SetCredentials("test.user", "test.passw0rd") + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) cl.UseOTESystem() r := cl.Request(map[string]interface{}{ "COMMAND": "CheckDomains", @@ -87,7 +90,7 @@ func TestRequestFlattenCommand(t *testing.T) { } func TestAutoIDNConvertCommand(t *testing.T) { - cl.SetCredentials("test.user", "test.passw0rd") + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) cl.UseOTESystem() r := cl.Request(map[string]interface{}{ "COMMAND": "CheckDomains", @@ -114,30 +117,9 @@ func TestAutoIDNConvertCommand(t *testing.T) { cl.UseLIVESystem() } -func TestGetSession1(t *testing.T) { - cl.Logout() - session, err := cl.GetSession() - if err == nil || session != "" { - t.Error("TestGetSession1: Expected no session, but found one.") - } -} - -func TestGetSesssion2(t *testing.T) { - sessid := "testSessionID12345678" - cl.SetSession(sessid) - session, err := cl.GetSession() - if err != nil { - t.Error("TestGetSession2: Expected not to run into error.") - } - if strings.Compare(session, sessid) != 0 { - t.Error("TestGetSession2: Expected session id not matching.") - } - cl.SetSession("") -} - func TestGetURL(t *testing.T) { url := cl.GetURL() - if strings.Compare(url, ISPAPI_CONNECTION_URL_LIVE) != 0 { + if strings.Compare(url, CNR_CONNECTION_URL_LIVE) != 0 { t.Error("TestGetURL: Expected url not matching.") } } @@ -172,98 +154,11 @@ func TestSetUserAgentModules(t *testing.T) { } func TestSetURL(t *testing.T) { - url := cl.SetURL(ISPAPI_CONNECTION_URL_PROXY).GetURL() - if strings.Compare(ISPAPI_CONNECTION_URL_PROXY, url) != 0 { + url := cl.SetURL(CNR_CONNECTION_URL_PROXY).GetURL() + if strings.Compare(CNR_CONNECTION_URL_PROXY, url) != 0 { t.Error("TestSetURL: Expected url not matching.") } - cl.SetURL(ISPAPI_CONNECTION_URL_LIVE) -} - -func TestSetOTP1(t *testing.T) { - cl.SetOTP("12345678") - tmp := cl.GetPOSTData(map[string]string{ - "COMMAND": "StatusAccount", - }) - if strings.Compare(tmp, "s_entity=54cd&s_otp=12345678&s_command=COMMAND%3DStatusAccount") != 0 { - t.Error("TestSetOTP1: Expected post data string not matching.") - } -} - -func TestSetOTP2(t *testing.T) { - cl.SetOTP("") - tmp := cl.GetPOSTData(map[string]string{ - "COMMAND": "StatusAccount", - }) - if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 { - t.Error("TestSetOTP2: Expected post data string not matching.") - } -} - -func TestSetSession1(t *testing.T) { - cl.SetSession("12345678") - tmp := cl.GetPOSTData(map[string]string{ - "COMMAND": "StatusAccount", - }) - if strings.Compare(tmp, "s_entity=54cd&s_session=12345678&s_command=COMMAND%3DStatusAccount") != 0 { - t.Error("TestSession1: Expected post data string not matching.") - } -} - -func TestSetSession2(t *testing.T) { - cl.SetRoleCredentials("myaccountid", "myrole", "mypassword") - cl.SetOTP("12345678") - cl.SetSession("12345678") - tmp := cl.GetPOSTData(map[string]string{ - "COMMAND": "StatusAccount", - }) - if strings.Compare(tmp, "s_entity=54cd&s_session=12345678&s_command=COMMAND%3DStatusAccount") != 0 { - t.Error("TestSession2: Expected post data string not matching.") - } -} - -func TestSetSession3(t *testing.T) { - cl.SetSession("") - tmp := cl.GetPOSTData(map[string]string{ - "COMMAND": "StatusAccount", - }) - if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 { - t.Error("TestSession3: Expected post data string not matching.") - } -} - -func TestSaveReuseSession(t *testing.T) { - sessionobj := map[string]interface{}{} - cl.SetSession("12345678") - cl.SaveSession(sessionobj) - cl2 := NewAPIClient() - cl2.ReuseSession(sessionobj) - tmp := cl2.GetPOSTData(map[string]string{ - "COMMAND": "StatusAccount", - }) - if strings.Compare(tmp, "s_entity=54cd&s_session=12345678&s_command=COMMAND%3DStatusAccount") != 0 { - t.Error("TestSaveReuseSession: Expected post data string not matching.") - } - cl.SetSession("") -} - -func TestSetRemoteIPAddress1(t *testing.T) { - cl.SetRemoteIPAddress("10.10.10.10") - tmp := cl.GetPOSTData(map[string]string{ - "COMMAND": "StatusAccount", - }) - if strings.Compare(tmp, "s_entity=54cd&s_remoteaddr=10.10.10.10&s_command=COMMAND%3DStatusAccount") != 0 { - t.Error("TestSetRemoteIPAddress1: Expected post data string not matching.") - } -} - -func TestSetRemoteIPAddress2(t *testing.T) { - cl.SetRemoteIPAddress("") - tmp := cl.GetPOSTData(map[string]string{ - "COMMAND": "StatusAccount", - }) - if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 { - t.Error("TestSetRemoteIPAddress2: Expected post data string not matching.") - } + cl.SetURL(CNR_CONNECTION_URL_LIVE) } func TestSetCredentials1(t *testing.T) { @@ -271,7 +166,7 @@ func TestSetCredentials1(t *testing.T) { tmp := cl.GetPOSTData(map[string]string{ "COMMAND": "StatusAccount", }) - if strings.Compare(tmp, "s_entity=54cd&s_login=myaccountid&s_pw=mypassword&s_command=COMMAND%3DStatusAccount") != 0 { + if strings.Compare(tmp, "s_login=myaccountid&s_pw=mypassword&s_command=COMMAND%3DStatusAccount") != 0 { t.Error("TestSetCredentials1: Expected post data string not matching.") } } @@ -281,7 +176,7 @@ func TestSetCredentials2(t *testing.T) { tmp := cl.GetPOSTData(map[string]string{ "COMMAND": "StatusAccount", }) - if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 { + if strings.Compare(tmp, "s_command=COMMAND%3DStatusAccount") != 0 { t.Error("TestSetCredentials2: Expected post data string not matching.") } } @@ -291,7 +186,7 @@ func TestSetRoleCredentials1(t *testing.T) { tmp := cl.GetPOSTData(map[string]string{ "COMMAND": "StatusAccount", }) - if strings.Compare(tmp, "s_entity=54cd&s_login=myaccountid%21myroleid&s_pw=mypassword&s_command=COMMAND%3DStatusAccount") != 0 { + if strings.Compare(tmp, "s_login=myaccountid%3Amyroleid&s_pw=mypassword&s_command=COMMAND%3DStatusAccount") != 0 { t.Error("TestSetRoleCredentials1: Expected post data string not matching.") } } @@ -301,7 +196,7 @@ func TestSetRoleCredentials2(t *testing.T) { tmp := cl.GetPOSTData(map[string]string{ "COMMAND": "StatusAccount", }) - if strings.Compare(tmp, "s_entity=54cd&s_command=COMMAND%3DStatusAccount") != 0 { + if strings.Compare(tmp, "s_command=COMMAND%3DStatusAccount") != 0 { t.Error("TestSetRoleCredentials2: Expected post data string not matching.") } } @@ -316,9 +211,9 @@ func TestSetProxy(t *testing.T) { } func TestSetReferer(t *testing.T) { - cl.SetReferer("https://www.hexonet.net/") + cl.SetReferer("https://www.centralnicreseller.com/") val, err := cl.GetReferer() - if err != nil || val != "https://www.hexonet.net/" { + if err != nil || val != "https://www.centralnicreseller.com/" { t.Error("TestSetReferer: referer not matching expected value") } cl.SetReferer("") @@ -327,7 +222,7 @@ func TestSetReferer(t *testing.T) { func TestUseHighPerformanceConnectionSetup(t *testing.T) { cl.UseHighPerformanceConnectionSetup() val := cl.GetURL() - if val != ISPAPI_CONNECTION_URL_PROXY { + if val != CNR_CONNECTION_URL_PROXY { t.Error("TestUseHighPerformanceConnectionSetup: couldn't activate high performance connection setup") } } @@ -335,34 +230,35 @@ func TestUseHighPerformanceConnectionSetup(t *testing.T) { func TestDefaultConnectionSetup(t *testing.T) { cl.UseDefaultConnectionSetup() val := cl.GetURL() - if val != ISPAPI_CONNECTION_URL_LIVE { + if val != CNR_CONNECTION_URL_LIVE { t.Error("TestDefaultConnectionSetup: couldn't activate default connection setup") } } -func TestLogin1(t *testing.T) { +func TestAccountStatus(t *testing.T) { cl.UseOTESystem() - cl.SetCredentials("test.user", "test.passw0rd") - cl.SetRemoteIPAddress("1.2.3.4") + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) cl.EnableDebugMode() - r := cl.Login() + cmd := map[string]interface{}{} + cmd["COMMAND"] = "StatusAccount" + r := cl.Request(cmd) + if r.GetDescription() == "Authorization failed" { + t.Error("TestAccountStatus: Please make sure correct credentials are provided") + } + if !r.IsSuccess() { - t.Error("TestLogin1: Expected response to be a success case.") + t.Error("TestAccountStatus: Expected response to be a success case.") } rec := r.GetRecord(0) if rec == nil { - t.Error("TestLogin1: Expected record not to be nil.") - } - d, err := rec.GetDataByKey("SESSION") - if err != nil || d == "" { - t.Error("TestLogin1: Expected session not to be empty.") + t.Error("TestAccountStatus: Expected record not to be nil.") } } -/*func TestLogin2(t *testing.T) { +func TestLogin(t *testing.T) { cl.UseOTESystem() - cl.SetRoleCredentials("test.user", "testrole", "test.passw0rd") - cl.SetRemoteIPAddress("1.2.3.4") + cl.EnableDebugMode() + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) r := cl.Login() if !r.IsSuccess() { t.Error("TestLogin2: Expected response to be a success case.") @@ -371,69 +267,87 @@ func TestLogin1(t *testing.T) { if rec == nil { t.Error("TestLogin2: Expected record not to be nil.") } - d, err := rec.GetDataByKey("SESSION") + d, err := rec.GetDataByKey("SESSIONID") if err != nil || d == "" { t.Error("TestLogin2: Expected session not to be empty.") } -}*/ +} func TestLogin3(t *testing.T) { - cl.SetCredentials("test.user", "WRONGPASSWORD") - cl.SetRemoteIPAddress("1.2.3.4") + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), "WRONGPASSWORD") + cl.EnableDebugMode() r := cl.Login() if !r.IsError() { t.Error("TestLogin3: Expected response to be an error case.") } } -// validate against mocked API response [login failed; http timeout] // need mocking -// validate against mocked API response [login succeeded; no session returned] // need mocking - -func TestLoginExtended(t *testing.T) { - cl.UseOTESystem() - cl.SetCredentials("test.user", "test.passw0rd") - cl.SetRemoteIPAddress("1.2.3.4") - r := cl.LoginExtended(map[string]string{ - "TIMEOUT": "60", - }) - if !r.IsSuccess() { - t.Error("TestLoginExtended: Expected response to be a success case.") +/** + * Make sure session is Cleaned up if password is provided after session is saved + */ +func TestLogin4(t *testing.T) { + sessionobj := map[string]interface{}{ + "socketcfg": map[string]string{ + "login": "myaccount", + "session": "abc", + }, } - rec := r.GetRecord(0) - if rec == nil { - t.Error("TestLoginExtended: Expected record not to be nil.") + // Initialize the first APIClient instance + cl.ReuseSession(sessionobj).SetCredentials("myaccountid", "password").EnableDebugMode() + + // Prepare the command map + cmd := map[string]string{ + "COMMAND": "StatusAccount", } - d, err := rec.GetDataByKey("SESSION") - if err != nil && d == "" { - t.Error("TestLoginExtended: Expected session not to be empty.") + + // Get the POST data from the second APIClient instance + tmp := cl.GetPOSTData(cmd) + + // Validate the result + if strings.Compare(tmp, "s_login=myaccountid&s_pw=password&s_command=COMMAND%3DStatusAccount") != 0 { + t.Error("TestLogin4: Expected post data string not matching." + tmp) } } +// validate against mocked API response [login failed; http timeout] // need mocking +// validate against mocked API response [login succeeded; no session returned] // need mocking + func TestLogout1(t *testing.T) { - r := cl.Logout() + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) + cl.UseOTESystem() + cl.EnableDebugMode() + r := cl.Login() + if !r.IsSuccess() { + t.Error("TestLogout1: Expected response to be a success case.") + } + r = cl.Logout() if !r.IsSuccess() { t.Error("TestLogout1: Expected response to be a success case.") } } -func TestLogout2(t *testing.T) { - tpl := R.NewResponse( - rtm.GetTemplate("login200"), - map[string]string{ - "COMMAND": "StartSession", - }, - nil, - ) - rec := tpl.GetRecord(0) - sessid, err := rec.GetDataByKey("SESSION") - if err != nil { - t.Error("TestLogout2: Expected not run into error.") +func TestSaveAndReuseSession(t *testing.T) { + // Initialize the first APIClient instance + sessionobj := make(map[string]interface{}) + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) + cl.Login() + cl.SaveSession(sessionobj) + + // Initialize the second APIClient instance + cl2 := NewAPIClient() + cl2.ReuseSession(sessionobj) + + // Prepare the command map + cmd := map[string]string{ + "COMMAND": "StatusAccount", } - cl.EnableDebugMode() - cl.SetSession(sessid) - r := cl.Logout() - if !r.IsError() { - t.Error("TestLogout2: Expected response to be an error case.") + + // Get the POST data from the second APIClient instance + tmp := cl2.GetPOSTData(cmd) + + // Validate the result + if !strings.Contains(tmp, "s_sessionid") { + t.Error("TestSaveReuseSession: Expected post data string to contain session ID.") } } @@ -446,16 +360,11 @@ func TestRequestNextResponsePage1(t *testing.T) { // nolint: gocyclo map[string]string{ "COMMAND": "QueryDomainList", "LIMIT": "2", - "FIRST": "0", }, ) cl.UseOTESystem() - cl.SetRoleCredentials("test.user", "", "test.passw0rd") - cl.SetRemoteIPAddress("1.2.3.4") - nr := cl.Login() - if !nr.IsSuccess() { - t.Error("TestRequestNextResponsePage1: Expected login response to be a success case.") - } + cl.DisableDebugMode() + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) nr, err := cl.RequestNextResponsePage(r) if err != nil { t.Error(err) @@ -514,7 +423,6 @@ func TestRequestNextResponsePage2(t *testing.T) { } func TestRequestNextResponsePage3(t *testing.T) { // nolint: gocyclo - cl.DisableDebugMode() r := R.NewResponse( rtm.GetTemplate("listP0"), map[string]string{ @@ -574,21 +482,42 @@ func TestRequestAllResponsePages(t *testing.T) { } func TestSetUserView(t *testing.T) { - cl.SetUserView("hexotestman.com") + cl.SetUserView("julia") + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) cmd := map[string]interface{}{} - cmd["COMMAND"] = "GetUserIndex" + cmd["COMMAND"] = "StatusAccount" r := cl.Request(cmd) if !r.IsSuccess() { - t.Error("TestSetUserView: Expected response to be a success case.") + t.Error("TestResetUserView: Expected response to be a success case.") + } + rec := r.GetRecord(0) + if rec == nil { + t.Error("TestLogin2: Expected record not to be nil.") } + d, err := rec.GetDataByKey("REGISTRAR") + if err != nil { + t.Errorf("Failed to get data by key 'REGISTRAR': %v", err) + } + assert.Equal(t, "julia", d) } func TestResetUserView(t *testing.T) { + cl.SetUserView("julia") cl.ResetUserView() + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) cmd := map[string]interface{}{} - cmd["COMMAND"] = "GetUserIndex" + cmd["COMMAND"] = "StatusAccount" r := cl.Request(cmd) if !r.IsSuccess() { t.Error("TestResetUserView: Expected response to be a success case.") } + rec := r.GetRecord(0) + if rec == nil { + t.Error("TestLogin2: Expected record not to be nil.") + } + d, err := rec.GetDataByKey("REGISTRAR") + if err != nil { + t.Errorf("Failed to get data by key 'REGISTRAR': %v", err) + } + assert.NotEqual(t, "julia", d) } diff --git a/column/column.go b/column/column.go index e68fdac..187af0e 100644 --- a/column/column.go +++ b/column/column.go @@ -32,6 +32,9 @@ func (c *Column) GetKey() string { // GetData method to return the column data func (c *Column) GetData() []string { + if c == nil { + return nil + } return c.data } @@ -40,7 +43,7 @@ func (c *Column) GetDataByIndex(idx int) (string, error) { if c.hasDataIndex(idx) { return c.data[idx], nil } - return "", errors.New("Index not found") + return "", errors.New("index not found") } // hasDataIndex method to check if the given data index exists diff --git a/customlogger/customlogger.go b/customlogger/customlogger.go index d945134..397ac53 100644 --- a/customlogger/customlogger.go +++ b/customlogger/customlogger.go @@ -9,8 +9,8 @@ package customlogger import ( "fmt" - L "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/logger" - R "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/response" + L "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/logger" + R "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/response" ) // Logger is a struct representing logger for API communication. diff --git a/demo/demo.go b/demo/demo.go new file mode 100644 index 0000000..6dae9ae --- /dev/null +++ b/demo/demo.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "os" + + CL "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/apiclient" +) + +func main() { + // sessionless API communication + fmt.Println("--- SESSIONLESS API COMMUNICATION ---") + cl := CL.NewAPIClient() + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) + cl.UseOTESystem() + cl.EnableDebugMode() + r := cl.Request(map[string]interface{}{ + "COMMAND": "StatusAccount", + }) + if r.IsSuccess() { + fmt.Println("Command succeeded.") + } else { + fmt.Println("Command failed.") + } + fmt.Println() + + // session based API communication + fmt.Println("--- SESSION BASED API COMMUNICATION ---") + cl = CL.NewAPIClient() + cl.SetCredentials(os.Getenv("CNR_TEST_USER"), os.Getenv("CNR_TEST_PASSWORD")) + cl.UseOTESystem() + cl.EnableDebugMode() + r = cl.Login() + if r.IsSuccess() { + fmt.Println("Login succeeded.") + r = cl.Request(map[string]interface{}{ + "COMMAND": "StatusAccount", + }) + if r.IsSuccess() { + fmt.Println("Command succeeded.") + r = cl.Logout() + if r.IsSuccess() { + fmt.Println("Logout succeeded.") + } else { + fmt.Println("Logout failed.") + } + } else { + fmt.Println("Command failed.") + } + } else { + fmt.Println("Login failed.") + } +} diff --git a/go.mod b/go.mod index f28981c..51aa574 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4 +module github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 go 1.22.1 // using this version to make it compatible with dnscontrol diff --git a/idntranslator/idntranslator.go b/idntranslator/idntranslator.go index fc04f86..5086e09 100644 --- a/idntranslator/idntranslator.go +++ b/idntranslator/idntranslator.go @@ -174,7 +174,11 @@ func decodeUnicodeEscapeSequences(unicodeString string) string { } // Combine the two code points into a single Unicode character // This is necessary because certain characters are represented by pairs of code points - runeValue := utf16.DecodeRune(rune(uint16(r1)), rune(uint16(r2))) + // Ensure the values are within the valid range for uint16 before conversion + if r1 > 0xFFFF || r2 > 0xFFFF { + return match // Return the original string if values are out of range + } + runeValue := utf16.DecodeRune(rune(r1), rune(r2)) // If the resulting character is invalid, keep the original surrogate pair if runeValue == utf8.RuneError { return match diff --git a/idntranslator/idntranslator_test.go b/idntranslator/idntranslator_test.go index 9f416fe..64915ee 100644 --- a/idntranslator/idntranslator_test.go +++ b/idntranslator/idntranslator_test.go @@ -3,7 +3,7 @@ package idntranslator_test import ( "testing" - "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/idntranslator" + "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/idntranslator" "github.com/stretchr/testify/assert" ) @@ -30,6 +30,12 @@ func TestConvert(t *testing.T) { {IDN: "münchen.de", PUNYCODE: "xn--mnchen-3ya.de"}, }, }, + { + domain: "faß.de", + expected: []Row{ + {IDN: "faß.de", PUNYCODE: "fass.de"}, + }, + }, { domain: "日本.co.jp", expected: []Row{ diff --git a/logger/logger.go b/logger/logger.go index 15a20a7..aa8b085 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -9,7 +9,7 @@ package logger import ( "fmt" - R "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/response" + R "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/response" ) // ilogger reflect basic interface for loggers diff --git a/package.json b/package.json index 8593c62..107e3ee 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "go-sdk", - "description": "GO/GOLANG SDK for the insanely fast HEXONET API", + "description": "GO/GOLANG SDK for the insanely fast CentralNic Reseller (fka RRPProxy) API", "version": "0.0.0-development", "private": true, "author": { "name": "Kai Schwarz", - "email": "kschwarz@hexonet.net" + "email": "kai.schwarz@centralnic.com" }, "license": "MIT", "engines": { @@ -17,16 +17,23 @@ "maintainers": [ { "name": "Kai Schwarz", - "email": "kschwarz@hexonet.net" + "email": "kai.schwarz@centralnic.com" + }, + { + "name": "Asif Nawaz", + "email": "asif.nawaz@centralnic.com" } ], + "scripts": { + "test-demo": "go mod tidy && go run demo/demo.go" + }, "keywords": [ - "hexonet", + "cnr", "domain", "api", "connector", - "isp", - "ispapi", + "cnic", + "centralnic reseller", "ssl", "cert", "dns", diff --git a/record/record.go b/record/record.go index c656d5a..cdbbf7f 100644 --- a/record/record.go +++ b/record/record.go @@ -28,6 +28,9 @@ func (c *Record) GetData() map[string]string { // GetDataByKey method to return the column data at the provided index func (c *Record) GetDataByKey(key string) (string, error) { + if c == nil { + return "", errors.New("record is nil") + } if c.hasData(key) { return c.data[key], nil } diff --git a/response/response.go b/response/response.go index 2de1986..aaa5f1b 100644 --- a/response/response.go +++ b/response/response.go @@ -13,10 +13,10 @@ import ( "strconv" "strings" - "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/column" - "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/record" - rp "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responseparser" - rt "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responsetranslator" + "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/column" + "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/record" + rp "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/responseparser" + rt "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/responsetranslator" ) // Response is a struct used to cover basic functionality to work with @@ -31,7 +31,17 @@ type Response struct { records []record.Record } -// NewResponse represents the constructor for struct Response. +const defaultCode = 421 + +// NewResponse creates a new Response object. +// It takes a raw string, a command map, and optional placeholder maps as parameters. +// The function replaces the "PASSWORD" value in the command map with "***" if it exists. +// It then translates the raw string using the command and placeholder maps. +// The function initializes a new Response object with the translated raw string, a hash value, +// the command map, empty column keys and columns, a record index of 0, and an empty records slice. +// If the hash value contains a "PROPERTY" key, the function adds columns and records to the Response object +// based on the values in the "PROPERTY" map. +// The function returns the newly created Response object. func NewResponse(raw string, cmd map[string]string, phs ...map[string]string) *Response { ph := map[string]string{} if len(phs) > 0 { @@ -88,20 +98,53 @@ func NewResponse(raw string, cmd map[string]string, phs ...map[string]string) *R return r } -// GetCode method to return the API response code +// GetCode returns the code associated with the response. +// If the response does not have a code or if the code is not a valid integer, +// it returns the default code. +// +// The code is retrieved from the response's hash, which is a map[string]interface{}. +// The code value is expected to be stored under the key "CODE" as a string. +// If the code value is not found or is not a string, the default code is returned. +// +// If an error occurs while converting the code string to an integer, +// the default code is returned and the error is logged or handled appropriately. +// +// Returns: +// - int: The code associated with the response. func (r *Response) GetCode() int { h := r.GetHash() - c, err := strconv.Atoi(h["CODE"].(string)) - if err == nil { - return c + if h == nil { + return defaultCode + } + + codeStr, ok := h["CODE"].(string) + if !ok { + // Log or handle the error appropriately + return defaultCode + } + + c, err := strconv.Atoi(codeStr) + if err != nil { + // Log or handle the error appropriately + return defaultCode } - return 421 + return c } // GetDescription method to return the API response description func (r *Response) GetDescription() string { h := r.GetHash() - return h["DESCRIPTION"].(string) + if h == nil { + return "" + } + + // Assuming there is a "DESCRIPTION" key in the hash + desc, ok := h["DESCRIPTION"].(string) + if !ok { + // Log or handle the error appropriately + return "" + } + return desc } // GetPlain method to return raw API response @@ -158,12 +201,23 @@ func (r *Response) IsTmpError() bool { // IsPending method to check if current operation is returned as pending func (r *Response) IsPending() bool { - h := r.GetHash() - if val, ok := h["PENDING"]; ok { - if val.(string) == "1" { - return true + cmd := r.GetCommand() + + // Check if the COMMAND is AddDomain (case-insensitive) + if command, ok := cmd["COMMAND"]; !ok || !strings.EqualFold(command, "AddDomain") { + return false + } + + // Retrieve the STATUS column and check if its data equals REQUESTED (case-insensitive) + status := r.GetColumn("STATUS") + if status != nil { + statusData, err := status.GetDataByIndex(0) + if err != nil { + return false } + return strings.EqualFold(statusData, "REQUESTED") } + return false } @@ -199,7 +253,7 @@ func (r *Response) GetColumnIndex(key string, index int) (string, error) { return d, nil } } - return "", errors.New("Column Data Index does not exist") + return "", errors.New("column Data Index does not exist") } // GetColumnKeys method to get the list of column names @@ -242,7 +296,7 @@ func (r *Response) GetCurrentPageNumber() (int, error) { if ferr == nil && limit > 0 { return int(math.Floor(float64(first)/float64(limit))) + 1, nil } - return 0, errors.New("Could not find current page number") + return 0, errors.New("could not find current page number") } // GetCurrentRecord method to get record of current record index @@ -263,14 +317,14 @@ func (r *Response) GetFirstRecordIndex() (int, error) { if err2 == nil { return idx, nil } - return 0, errors.New("Could not find first record index") + return 0, errors.New("could not find first record index") } } tlen := len(r.records) if tlen > 1 { return 0, nil } - return 0, errors.New("Could not find first record index") + return 0, errors.New("could not find first record index") } // GetLastRecordIndex method to get last record index of the current list query @@ -283,14 +337,14 @@ func (r *Response) GetLastRecordIndex() (int, error) { if err2 == nil { return idx, nil } - return 0, errors.New("Could not find last record index") + return 0, errors.New("could not find last record index") } } tlen := r.GetRecordsCount() if tlen > 0 { return (tlen - 1), nil } - return 0, errors.New("Could not find last record index") + return 0, errors.New("could not find last record index") } // GetListHash method to get Response as List Hash including useful meta data for tables @@ -322,7 +376,7 @@ func (r *Response) GetNextRecord() *record.Record { func (r *Response) GetNextPageNumber() (int, error) { cp, err := r.GetCurrentPageNumber() if err != nil { - return 0, errors.New("Could not find next page number") + return 0, errors.New("could not find next page number") } page := cp + 1 pages := r.GetNumberOfPages() @@ -385,7 +439,7 @@ func (r *Response) GetPreviousPageNumber() (int, error) { } pp := cp - 1 if pp < 1 { - return 0, errors.New("Could not find previous page number") + return 0, errors.New("could not find previous page number") } return pp, nil } @@ -472,14 +526,15 @@ func (r *Response) RewindRecordList() *Response { return r } -// hasColumn method to check if the given column exists in column list +// hasColumn method to check if the given column exists in column list (case-insensitive) func (r *Response) hasColumn(key string) (int, bool) { + lowerKey := strings.ToLower(key) for i, k := range r.columnkeys { - if k == key { + if strings.ToLower(k) == lowerKey { return i, true } } - return 0, false + return -1, false } // hasCurrentRecord method to check if the record on current record index exists diff --git a/response/response_test.go b/response/response_test.go index b25e6af..bff9199 100644 --- a/response/response_test.go +++ b/response/response_test.go @@ -8,8 +8,7 @@ import ( "strings" "testing" - RP "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responseparser" - RTM "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responsetemplatemanager" + RTM "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/responsetemplatemanager" ) var rtm = RTM.GetInstance() @@ -17,11 +16,15 @@ var rtm = RTM.GetInstance() func TestMain(m *testing.M) { rtm.AddTemplate( "login200", - "[RESPONSE]\r\nPROPERTY[SESSION][0]=h8JLZZHdF2WgWWXlwbKWzEG3XrzoW4yshhvtqyg0LCYiX55QnhgYX9cB0W4mlpbx\r\nDESCRIPTION=Command completed successfully\r\nCODE=200\r\nQUEUETIME=0\r\nRUNTIME=0.169\r\nEOF\r\n", + "[RESPONSE]\r\nproperty[expiration date][0] = 2024-09-19 10:52:51\r\nproperty[sessionid][0] = bb7a884b09b9a674fb4a22211758ce87\r\ndescription = Command completed successfully\r\ncode = 200\r\nqueuetime = 0.004\r\nruntime = 0.023\r\nEOF\r\n", ) rtm.AddTemplate( "listP0", - "[RESPONSE]\r\nPROPERTY[TOTAL][0]=2701\r\nPROPERTY[FIRST][0]=0\r\nPROPERTY[DOMAIN][0]=0-60motorcycletimes.com\r\nPROPERTY[DOMAIN][1]=0-be-s01-0.com\r\nPROPERTY[COUNT][0]=2\r\nPROPERTY[LAST][0]=1\r\nPROPERTY[LIMIT][0]=2\r\nDESCRIPTION=Command completed successfully\r\nCODE=200\r\nQUEUETIME=0\r\nRUNTIME=0.023\r\nEOF\r\n", + "[RESPONSE]\r\nproperty[total][0] = 4\r\nproperty[first][0] = 0\r\nproperty[domain][0] = cnic-ssl-test1.com\r\nproperty[domain][1] = cnic-ssl-test2.com\r\nproperty[count][0] = 2\r\nproperty[last][0] = 1\r\nproperty[limit][0] = 2\r\ndescription = Command completed successfully\r\ncode = 200\r\nqueuetime = 0\r\nruntime = 0.007\r\nEOF\r\n", + ) + rtm.AddTemplate( + "pendingRegistration", + "[RESPONSE]\r\ncode = 200\r\ndescription = Command completed successfully\r\nruntime = 0.44\r\nqueuetime = 0\r\n\r\nproperty[status][0] = REQUESTED\r\nproperty[updated date][0] = 2023-05-22 12:14:31.0\r\nproperty[zone][0] = se\r\nEOF\r\n", ) rtm.AddTemplate( "OK", @@ -96,23 +99,6 @@ func TestGetFirstRecordIndex1(t *testing.T) { } } -func TestGetFirstRecordIndex2(t *testing.T) { - h := RP.Parse(rtm.GetTemplate("OK")) - h["PROPERTY"] = map[string][]string{ - "DOMAIN": {"mydomain1.com", "mydomain2.com"}, - } - serialized := RP.Serialize(h) - r := NewResponse(serialized, map[string]string{"COMMAND": "QueryDomainList"}) - v, err := r.GetFirstRecordIndex() - fmt.Println(serialized) - if err != nil { - t.Error("TestGetFirstRecordIndex2: Expected not to run into error.") - } - if v != 0 { - t.Error(fmt.Printf("TestGetFirstRecordIndex2: Expected index value '%d' to be '0'.", v)) - } -} - func TestGetColumms(t *testing.T) { plain := rtm.GetTemplate("listP0") r := NewResponse(plain, map[string]string{"COMMAND": "QueryDomainList"}) @@ -129,7 +115,7 @@ func TestGetColumnIndex1(t *testing.T) { if err != nil { t.Error("Expected not to run into error.") } - if strings.Compare(data, "0-60motorcycletimes.com") != 0 { + if strings.Compare(data, "cnic-ssl-test1.com") != 0 { t.Error("Expected domain name not matching.") } } @@ -171,11 +157,11 @@ func TestGetCurrentRecord(t *testing.T) { d := rec.GetData() expected := map[string]string{ "COUNT": "2", - "DOMAIN": "0-60motorcycletimes.com", + "DOMAIN": "cnic-ssl-test1.com", "FIRST": "0", "LAST": "1", "LIMIT": "2", - "TOTAL": "2701", + "TOTAL": "4", } eq := reflect.DeepEqual(d, expected) if !eq { @@ -221,7 +207,7 @@ func TestGetNextRecord(t *testing.T) { plain := rtm.GetTemplate("listP0") r := NewResponse(plain, map[string]string{"COMMAND": "QueryDomainList"}) rec := r.GetNextRecord() - expected := map[string]string{"DOMAIN": "0-be-s01-0.com"} + expected := map[string]string{"DOMAIN": "cnic-ssl-test2.com"} if !reflect.DeepEqual(rec.GetData(), expected) { t.Error("Expected record data not matching.") } @@ -260,11 +246,11 @@ func TestGetPreviousRecord(t *testing.T) { d := r.GetPreviousRecord().GetData() expected := map[string]string{ "COUNT": "2", - "DOMAIN": "0-60motorcycletimes.com", + "DOMAIN": "cnic-ssl-test1.com", "FIRST": "0", "LAST": "1", "LIMIT": "2", - "TOTAL": "2701", + "TOTAL": "4", } if !reflect.DeepEqual(d, expected) { t.Error("Expected previous record data not matching.") @@ -316,21 +302,6 @@ func TestGetLastRecordIndex1(t *testing.T) { } } -func TestGetLastRecordIndex2(t *testing.T) { - h := RP.Parse(rtm.GetTemplate("OK")) - h["PROPERTY"] = map[string][]string{ - "DOMAIN": {"mydomain1.com", "mydomain2.com"}, - } - r := NewResponse(RP.Serialize(h), map[string]string{"COMMAND": "QueryDomainList"}) - lr, err := r.GetLastRecordIndex() - if err != nil { - t.Error("Expected not to run into error.") - } - if lr != 1 { - t.Error(fmt.Printf("Expected last record index '%d' to be '1'.", lr)) - } -} - func TestGetNextPageNumber1(t *testing.T) { plain := rtm.GetTemplate("OK") r := NewResponse(plain, map[string]string{"COMMAND": "QueryDomainList"}) @@ -399,3 +370,12 @@ func TestRewindRecordList(t *testing.T) { t.Error("Expected previous record to be nil.") } } + +func TestIsPending(t *testing.T) { + plain := rtm.GetTemplate("pendingRegistration") + r := NewResponse(plain, map[string]string{"COMMAND": "AddDomain"}) + fmt.Print(r.IsPending()) + if got := r.IsPending(); got != true { + t.Errorf("isPending() = %v, want true", got) + } +} diff --git a/responseparser/responseparser.go b/responseparser/responseparser.go index 47b40af..56b7041 100644 --- a/responseparser/responseparser.go +++ b/responseparser/responseparser.go @@ -8,9 +8,7 @@ package responseparser import ( - "fmt" "regexp" - "sort" "strings" ) @@ -51,45 +49,3 @@ func Parse(r string) map[string]interface{} { } return hash } - -// Serialize method to serialize API response hash format back to string -func Serialize(hash map[string]interface{}) string { - var plain strings.Builder - plain.WriteString("[RESPONSE]") - keys := []string{} - for k := range hash { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - if strings.Compare(k, "PROPERTY") == 0 { - p := hash[k].(map[string][]string) - keys2 := []string{} - for k2 := range p { - keys2 = append(keys2, k2) - } - sort.Strings(keys2) - for _, k2 := range keys2 { - v2 := p[k2] - for i, v3 := range v2 { - plain.WriteString("\r\nPROPERTY[") - plain.WriteString(k2) - plain.WriteString("][") - plain.WriteString(fmt.Sprintf("%d", i)) - plain.WriteString("]=") - plain.WriteString(v3) - } - } - } else { - tmp := hash[k].(string) - if len(tmp) > 0 { - plain.WriteString("\r\n") - plain.WriteString(k) - plain.WriteString("=") - plain.WriteString(tmp) - } - } - } - plain.WriteString("\r\nEOF\r\n") - return plain.String() -} diff --git a/responseparser/responseparser_test.go b/responseparser/responseparser_test.go deleted file mode 100644 index c06289c..0000000 --- a/responseparser/responseparser_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package responseparser - -import ( - "strings" - "testing" -) - -func TestSerialize1(t *testing.T) { - r := Parse("[RESPONSE]\r\nCODE=200\r\nDESCRIPTION=Command completed successfully\r\nEOF\r\n") - r["PROPERTY"] = map[string][]string{ - "DOMAIN": {"mydomain1.com", "mydomain2.com", "mydomain3.com"}, - "RATING": {"1", "2", "3"}, - "SUM": {"3"}, - } - serialized := Serialize(r) - expected := "[RESPONSE]\r\nCODE=200\r\nDESCRIPTION=Command completed successfully\r\nPROPERTY[DOMAIN][0]=mydomain1.com\r\nPROPERTY[DOMAIN][1]=mydomain2.com\r\nPROPERTY[DOMAIN][2]=mydomain3.com\r\nPROPERTY[RATING][0]=1\r\nPROPERTY[RATING][1]=2\r\nPROPERTY[RATING][2]=3\r\nPROPERTY[SUM][0]=3\r\nEOF\r\n" - if strings.Compare(serialized, expected) != 0 { - t.Error("TestSerialize1: Expected string not matching serialized format.") - } -} - -func TestSerialize2(t *testing.T) { - expected := "[RESPONSE]\r\nCODE=200\r\nDESCRIPTION=Command completed successfully\r\nEOF\r\n" - serialized := Serialize(Parse(expected)) - if strings.Compare(serialized, expected) != 0 { - t.Error("TestSerialize2: Expected string not matching serialized format.") - } -} - -func TestSerialize3(t *testing.T) { - // this case shouldn't happen, otherwise we have an API-side issue - h := Parse("[RESPONSE]\r\nCODE=200\r\nDESCRIPTION=Command completed successfully\r\nEOF\r\n") - delete(h, "CODE") - delete(h, "DESCRIPTION") - serialized := Serialize(h) - expected := "[RESPONSE]\r\nEOF\r\n" - if strings.Compare(serialized, expected) != 0 { - t.Error("ETestSerialize3: xpected string not matching serialized format.") - } -} - -func TestSerialize4(t *testing.T) { - h := Parse("[RESPONSE]\r\nCODE=200\r\nDESCRIPTION=Command completed successfully\r\nEOF\r\n") - h["QUEUETIME"] = "0" - h["RUNTIME"] = "0.12" - serialized := Serialize(h) - expected := "[RESPONSE]\r\nCODE=200\r\nDESCRIPTION=Command completed successfully\r\nQUEUETIME=0\r\nRUNTIME=0.12\r\nEOF\r\n" - if strings.Compare(serialized, expected) != 0 { - t.Error("TestSerialize4: Expected string not matching serialized format.") - } -} diff --git a/responsetemplatemanager/responsetemplatemanager.go b/responsetemplatemanager/responsetemplatemanager.go index 4e3ecdd..d47b49d 100644 --- a/responsetemplatemanager/responsetemplatemanager.go +++ b/responsetemplatemanager/responsetemplatemanager.go @@ -10,7 +10,7 @@ import ( "strings" "sync" - rp "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responseparser" + rp "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/responseparser" ) // ResponseTemplateManager is a struct used to cover basic functionality to work with diff --git a/responsetemplatemanager/responsetemplatemanager_test.go b/responsetemplatemanager/responsetemplatemanager_test.go index 9eab24d..c0b99f2 100644 --- a/responsetemplatemanager/responsetemplatemanager_test.go +++ b/responsetemplatemanager/responsetemplatemanager_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - RP "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responseparser" + RP "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/responseparser" ) var rtm = GetInstance() @@ -13,11 +13,11 @@ var rtm = GetInstance() func TestMain(m *testing.M) { rtm.AddTemplate( "login200", - "[RESPONSE]\r\nPROPERTY[SESSION][0]=h8JLZZHdF2WgWWXlwbKWzEG3XrzoW4yshhvtqyg0LCYiX55QnhgYX9cB0W4mlpbx\r\nDESCRIPTION=Command completed successfully\r\nCODE=200\r\nQUEUETIME=0\r\nRUNTIME=0.169\r\nEOF\r\n", + "[RESPONSE]\r\nproperty[expiration date][0] = 2024-09-19 10:52:51\r\nproperty[sessionid][0] = bb7a884b09b9a674fb4a22211758ce87\r\ndescription = Command completed successfully\r\ncode = 200\r\nqueuetime = 0.004\r\nruntime = 0.023\r\nEOF\r\n", ) rtm.AddTemplate( "listP0", - "[RESPONSE]\r\nPROPERTY[TOTAL][0]=2701\r\nPROPERTY[FIRST][0]=0\r\nPROPERTY[DOMAIN][0]=0-60motorcycletimes.com\r\nPROPERTY[DOMAIN][1]=0-be-s01-0.com\r\nPROPERTY[COUNT][0]=2\r\nPROPERTY[LAST][0]=1\r\nPROPERTY[LIMIT][0]=2\r\nDESCRIPTION=Command completed successfully\r\nCODE=200\r\nQUEUETIME=0\r\nRUNTIME=0.023\r\nEOF\r\n", + "[RESPONSE]\r\nproperty[total][0] = 4\r\nproperty[first][0] = 0\r\nproperty[domain][0] = cnic-ssl-test1.com\r\nproperty[domain][1] = cnic-ssl-test2.com\r\nproperty[count][0] = 2\r\nproperty[last][0] = 1\r\nproperty[limit][0] = 2\r\ndescription = Command completed successfully\r\ncode = 200\r\nqueuetime = 0\r\nruntime = 0.007\r\nEOF\r\n", ) rtm.AddTemplate( "OK", diff --git a/responsetranslator/responsetranslator.go b/responsetranslator/responsetranslator.go index 488a64f..847d435 100644 --- a/responsetranslator/responsetranslator.go +++ b/responsetranslator/responsetranslator.go @@ -10,30 +10,24 @@ import ( "regexp" "strings" - RTM "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responsetemplatemanager" + RTM "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/responsetemplatemanager" ) type ResponseTranslator struct { } var descriptionRegexMap = map[string]string{ - // HX - "Authorization failed; Operation forbidden by ACL": "Authorization failed; Used Command `{COMMAND}` not white-listed by your Access Control List", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (clientTransferProhibited)/WRONG AUTH": "This Domain is locked and the given Authorization Code is wrong. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (clientTransferProhibited)": "This Domain is locked. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (requested)": "Registration of this Domain Name has not yet completed. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (requestedcreate)": "Registration of this Domain Name has not yet completed. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (requesteddelete)": "Deletion of this Domain Name has been requested. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (pendingdelete)": "Deletion of this Domain Name is pending. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY WRONG AUTH": "The given Authorization Code is wrong. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY AGE OF THE DOMAIN": "This Domain Name is within 60 days of initial registration. Initiating a Transfer is therefore impossible.", - "Attribute value is not unique; DOMAIN is already assigned to your account": "You cannot transfer a domain that is already on your account at the registrar's system.", + // HX | CNR? + "Authorization failed; Operation forbidden by ACL": "Authorization failed; Used Command `{COMMAND}` not white-listed by your Access Control List", // CNR + "Domain status does not allow for operation": "This Domain is locked. Initiating a Transfer is therefore impossible.", + "Authorization failed [Invalid authorization information]": "The given Authorization Code is wrong. Initiating a Transfer is therefore impossible.", "Missing required attribute; premium domain name. please provide required parameters": "Confirm the Premium pricing by providing the necessary premium domain price data.", } var descriptionRegexMapSkipQuote = map[string]string{ - // HX + // all cases goes here that need translation e.g. + // HX | CNR? `Invalid attribute value syntax; resource record \[(.+)\]`: "Invalid Syntax for DNSZone Resource Record: $1", `Missing required attribute; CLASS(?:=| \[MUST BE )PREMIUM_([\w\+]+)[\s\]]`: "Confirm the Premium pricing by providing the parameter CLASS with the value PREMIUM_$1.", `Syntax error in Parameter DOMAIN \((.+)\)`: "The Domain Name $1 is invalid.", @@ -91,6 +85,7 @@ func Translate(raw string, cmd map[string]string, phs ...map[string]string) stri // Escape the regex pattern and attempt to find a match escapedRegex := regexp.QuoteMeta(regex) data = FindMatch(escapedRegex, newraw, val, cmd, ph) + // If a match is found, exit the inner loop if len(data) > 0 { newraw = data @@ -118,16 +113,18 @@ func FindMatch(regex string, newraw string, val string, cmd map[string]string, p // NOTE: we match if the description starts with the given description // it would also match if it is followed by additional text ret := "" - qregex := regexp.MustCompile("(?i)description\\s*=\\s*" + regex + "([^\\r\\n]+)?") + qregex := regexp.MustCompile(`(?i)description[\s]*=[\s]*` + regex + `([^\r\n]+)?`) if qregex.FindString(newraw) != "" { - // If "COMMAND" exists in cmd, replace "{COMMAND}" in val - myval, ok := cmd["COMMAND"] - if ok { - val = strings.ReplaceAll(val, "{COMMAND}", myval) + // Replace any placeholders dynamically in val using cmd and ph maps + for key, replaceVal := range cmd { + val = strings.ReplaceAll(val, "{"+key+"}", replaceVal) + } + for key, replaceVal := range ph { + val = strings.ReplaceAll(val, "{"+key+"}", replaceVal) } - // If $newraw matches $qregex, replace with "description=" . $val + // If $newraw matches $qregex, replace with "description=" + val tmp := qregex.ReplaceAllString(newraw, "description="+val) if newraw != tmp { ret = tmp @@ -139,14 +136,31 @@ func FindMatch(regex string, newraw string, val string, cmd map[string]string, p return ret } - // Generic replacing of placeholder vars + // Compile the regular expression to match placeholders vregex := regexp.MustCompile(`\{[^}]+\}`) - if vregex.FindString(ret) != "" { - for tkey, tval := range ph { - ret = strings.ReplaceAll(ret, "{"+tkey+"}", tval) + + // Keep track of whether any replacements were made + replacementsMade := false + + // Replace placeholders with corresponding values from the map + newret := ret + if ret == "" { + newret = newraw + } + newret = vregex.ReplaceAllStringFunc(newret, func(match string) string { + placeholder := match[1 : len(match)-1] // Extract the placeholder name + + // Check if the placeholder exists in the map + if value, exists := ph[placeholder]; exists { + replacementsMade = true + return value } + return "" // Remove placeholder if not found + }) - ret = vregex.ReplaceAllString(ret, "") + // Return an newret if replacements were made + if replacementsMade { + return newret } return ret diff --git a/responsetranslator/responsetranslator_test.go b/responsetranslator/responsetranslator_test.go index 45f227b..373181c 100644 --- a/responsetranslator/responsetranslator_test.go +++ b/responsetranslator/responsetranslator_test.go @@ -3,17 +3,17 @@ package responsetranslator_test import ( "testing" - "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/response" - rp "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responseparser" - rt "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v4/responsetranslator" + "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/response" + rp "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/responseparser" + rt "github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5/responsetranslator" ) func TestTranslate(t *testing.T) { - cmd := map[string]string{"COMMAND": "CheckDomainTransfer", "DOMAIN": "my–domain.com", "AUTH": "blablabla"} + cmd := map[string]string{"COMMAND": "CheckDomainTransfer", "DOMAIN": "google.com", "AUTH": "blablabla"} // Test ACL error translation t.Run("ACLTranslation", func(t *testing.T) { - raw := "[RESPONSE]\r\ncode=530\r\ndescription=Authorization failed; Operation forbidden by ACL\r\nEOF\r\n" + raw := "[RESPONSE]\r\ncode = 530\r\ndescription = Authorization failed; Operation forbidden by ACL\r\nEOF\r\n" expected := "Authorization failed; Used Command `CheckDomainTransfer` not white-listed by your Access Control List" newraw := rt.Translate(raw, cmd) r := &response.Response{ @@ -21,32 +21,27 @@ func TestTranslate(t *testing.T) { Hash: rp.Parse(newraw), } h := r.GetHash() + if h["DESCRIPTION"] != expected { t.Errorf("Expected: %s, got: %s", expected, h["DESCRIPTION"]) } }) - // Test CheckDomainTransfer translations t.Run("CheckDomainTransferTranslation", func(t *testing.T) { testCases := map[string]string{ - // Add more test cases as needed - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (clientTransferProhibited)": "This Domain is locked. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (requested)": "Registration of this Domain Name has not yet completed. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (requestedcreate)": "Registration of this Domain Name has not yet completed. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (requesteddelete)": "Deletion of this Domain Name has been requested. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (pendingdelete)": "Deletion of this Domain Name is pending. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY WRONG AUTH": "The given Authorization Code is wrong. Initiating a Transfer is therefore impossible.", - "Request is not available; DOMAIN TRANSFER IS PROHIBITED BY AGE OF THE DOMAIN": "This Domain Name is within 60 days of initial registration. Initiating a Transfer is therefore impossible.", + "Domain status does not allow for operation": "This Domain is locked. Initiating a Transfer is therefore impossible.", + "Authorization failed [Invalid authorization information]": "The given Authorization Code is wrong. Initiating a Transfer is therefore impossible.", } for input, expected := range testCases { - raw := "[RESPONSE]\r\ncode=219\r\ndescription=" + input + "\r\nEOF\r\n" + raw := "[RESPONSE]\r\ncode = 219\r\ndescription = " + input + "\r\nEOF\r\n" newraw := rt.Translate(raw, cmd) r := &response.Response{ Raw: newraw, Hash: rp.Parse(newraw), } h := r.GetHash() + if h["DESCRIPTION"] != expected { t.Errorf("Expected: %s, got: %s", expected, h["DESCRIPTION"]) } @@ -57,9 +52,8 @@ func TestTranslate(t *testing.T) { t.Run("Translate", func(t *testing.T) { testCases := map[string]string{ // Add more test cases as needed - "[RESPONSE]\r\ncode=219\r\ndescription=Request is not available; DOMAIN TRANSFER IS PROHIBITED BY STATUS (clientTransferProhibited)\r\nEOF\r\n": "This Domain is locked. Initiating a Transfer is therefore impossible.", - "[RESPONSE]\r\ncode=505\r\ndescription=Invalid attribute value syntax; resource record [213123 A 1.2.4.5asdfa]\r\nEOF\r\n": "Invalid Syntax for DNSZone Resource Record: 213123 A 1.2.4.5asdfa", - "[RESPONSE]\r\ncode=505\r\ndescription=Syntax error in Parameter DOMAIN (my–domain.de)\r\nEOF\r\n": "The Domain Name my–domain.de is invalid.", + "[RESPONSE]\r\ncode = 505\r\ndescription = Invalid attribute value syntax; resource record [213123 A 1.2.4.5asdfa]\r\nEOF\r\n": "Invalid Syntax for DNSZone Resource Record: 213123 A 1.2.4.5asdfa", + "[RESPONSE]\r\ncode = 505\r\ndescription = Syntax error in Parameter DOMAIN (my–domain.de)\r\nEOF\r\n": "The Domain Name my–domain.de is invalid.", } for input, expected := range testCases { @@ -69,6 +63,10 @@ func TestTranslate(t *testing.T) { Hash: rp.Parse(newraw), } h := r.GetHash() + + // Debug output + // fmt.Printf("Input: %s\nExpected: %s\nGot: %s\n", input, expected, h["DESCRIPTION"]) + if h["DESCRIPTION"] != expected { t.Errorf("Expected: %s, got: %s", expected, h["DESCRIPTION"]) } diff --git a/socketconfig/socketconfig.go b/socketconfig/socketconfig.go index 2f2c39b..d87db2b 100644 --- a/socketconfig/socketconfig.go +++ b/socketconfig/socketconfig.go @@ -13,25 +13,19 @@ import ( // SocketConfig is a struct representing connection settings used as POST data for http request against the insanely fast HEXONET backend API. type SocketConfig struct { - entity string login string - otp string pw string - remoteaddr string session string - user string + persistent string } // NewSocketConfig represents the constructor for struct SocketConfig. func NewSocketConfig() *SocketConfig { sc := &SocketConfig{ - entity: "", login: "", - otp: "", + persistent: "", pw: "", - remoteaddr: "", session: "", - user: "", } return sc } @@ -40,46 +34,28 @@ func NewSocketConfig() *SocketConfig { // POST request of type "application/x-www-form-urlencoded" func (s *SocketConfig) GetPOSTData() string { var tmp strings.Builder - if len(s.entity) > 0 { - tmp.WriteString(url.QueryEscape("s_entity")) - tmp.WriteString("=") - tmp.WriteString(url.QueryEscape(s.entity)) - tmp.WriteString("&") - } if len(s.login) > 0 { tmp.WriteString(url.QueryEscape("s_login")) tmp.WriteString("=") tmp.WriteString(url.QueryEscape(s.login)) tmp.WriteString("&") } - if len(s.otp) > 0 { - tmp.WriteString(url.QueryEscape("s_otp")) - tmp.WriteString("=") - tmp.WriteString(url.QueryEscape(s.otp)) - tmp.WriteString("&") - } if len(s.pw) > 0 { tmp.WriteString(url.QueryEscape("s_pw")) tmp.WriteString("=") tmp.WriteString(url.QueryEscape(s.pw)) tmp.WriteString("&") } - if len(s.remoteaddr) > 0 { - tmp.WriteString(url.QueryEscape("s_remoteaddr")) - tmp.WriteString("=") - tmp.WriteString(url.QueryEscape(s.remoteaddr)) - tmp.WriteString("&") - } if len(s.session) > 0 { - tmp.WriteString(url.QueryEscape("s_session")) + tmp.WriteString(url.QueryEscape("s_sessionid")) tmp.WriteString("=") tmp.WriteString(url.QueryEscape(s.session)) tmp.WriteString("&") } - if len(s.user) > 0 { - tmp.WriteString(url.QueryEscape("s_user")) + if len(s.persistent) > 0 { + tmp.WriteString(url.QueryEscape("persistent")) tmp.WriteString("=") - tmp.WriteString(url.QueryEscape(s.user)) + tmp.WriteString(url.QueryEscape(s.persistent)) tmp.WriteString("&") } return tmp.String() @@ -90,22 +66,21 @@ func (s *SocketConfig) GetSession() string { return s.session } -// GetSystemEntity method to return the API system entity currently in use. -func (s *SocketConfig) GetSystemEntity() string { - return s.entity +// GetLogin method to return the login id currently in use. +func (s *SocketConfig) GetLogin() string { + return s.login } // SetLogin method to set username to use for api communication func (s *SocketConfig) SetLogin(value string) *SocketConfig { - s.session = "" s.login = value return s } -// SetOTP method to set one time password to use for api communication -func (s *SocketConfig) SetOTP(value string) *SocketConfig { +// Persistent method for session to use for api communication +func (s *SocketConfig) SetPersistent() *SocketConfig { s.session = "" - s.otp = value + s.persistent = "1" return s } @@ -116,33 +91,11 @@ func (s *SocketConfig) SetPassword(value string) *SocketConfig { return s } -// SetRemoteAddress method to set remote ip address to be submitted to the HEXONET API. -// This ip address is being considered when you have ip filter settings activated. -// To reset this, simply provide an empty string as parameter. -func (s *SocketConfig) SetRemoteAddress(value string) *SocketConfig { - s.remoteaddr = value - return s -} - // SetSession method to set a API session id to use for api communication instead of credentials // which is basically required in case you plan to use session based communication or if you want to use 2FA func (s *SocketConfig) SetSession(sessionid string) *SocketConfig { - s.login = "" s.pw = "" - s.otp = "" + s.persistent = "" s.session = sessionid return s } - -// SetSystemEntity method to set the system to use e.g. 1234 -> OT&E System, 54cd -> LIVE System -func (s *SocketConfig) SetSystemEntity(value string) *SocketConfig { - s.entity = value - return s -} - -// SetUser method to set an user account (must be subuser account of your login user) to use for API communication -// use this if you want to make changes on that subuser account or if you want to have his data view -func (s *SocketConfig) SetUser(username string) *SocketConfig { - s.user = username - return s -}