api-structs #117

Merged
lash merged 35 commits from api-structs into master 2024-10-24 15:53:46 +02:00
8 changed files with 295 additions and 127 deletions
Showing only changes of commit 847b91ca9e - Show all commits

View File

@ -1,10 +1,7 @@
package config package config
const ( const (
CreateAccountURL = "https://custodial.sarafu.africa/api/account/create" CreateAccountURL = "http://localhost:5003/api/v2/account/create"
TrackStatusURL = "https://custodial.sarafu.africa/api/track/" BalanceURL = "https://custodial.sarafu.africa/api/account/status/"
BalanceURL = "https://custodial.sarafu.africa/api/account/status/" TrackURL = "http://localhost:5003/api/v2/account/status"
) )

View File

@ -2,6 +2,7 @@ package server
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"time" "time"
@ -10,10 +11,16 @@ import (
"git.grassecon.net/urdt/ussd/internal/models" "git.grassecon.net/urdt/ussd/internal/models"
) )
var apiResponse struct {
Ok bool `json:"ok"`
Outdated
Review

why is this needed?

why is this needed?

Well,at the point of making the api call and receiving a response back, we don't really know the response that will be sent back,it can either be an ErrResponse or an OKResponse but we are certain that the ok and description field will be present.So the OK field in the ApiResponse will be used to decide which type to Unmarshal on.

if apiResponse.Ok {
		err = json.Unmarshal([]byte(body), &okResponse)
		if err != nil {
			errResponse.Description = err.Error()
			return nil, &errResponse
		}
		return &okResponse, nil
	} else {
		err := json.Unmarshal([]byte(body), &errResponse)
		if err != nil {
			errResponse.Description = err.Error()
			return nil, &errResponse
		}
		return nil, &errResponse
	}
Well,at the point of making the api call and receiving a response back, we don't really know the response that will be sent back,it can either be an ErrResponse or an OKResponse but we are certain that the ok and description field will be present.So the OK field in the ApiResponse will be used to decide which type to Unmarshal on. ``` if apiResponse.Ok { err = json.Unmarshal([]byte(body), &okResponse) if err != nil { errResponse.Description = err.Error() return nil, &errResponse } return &okResponse, nil } else { err := json.Unmarshal([]byte(body), &errResponse) if err != nil { errResponse.Description = err.Error() return nil, &errResponse } return nil, &errResponse } ```
Description string `json:"description"`
}
type AccountServiceInterface interface { type AccountServiceInterface interface {
CheckBalance(publicKey string) (*models.BalanceResponse, error) CheckBalance(publicKey string) (*models.BalanceResponse, error)
CreateAccount() (*models.AccountResponse, error) CreateAccount() (*OKResponse, *ErrResponse)
CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error)
TrackAccountStatus(publicKey string) (*OKResponse, *ErrResponse)
} }
type AccountService struct { type AccountService struct {
@ -22,8 +29,6 @@ type AccountService struct {
type TestAccountService struct { type TestAccountService struct {
} }
// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
//
// Parameters: // Parameters:
// - trackingId: A unique identifier for the account.This should be obtained from a previous call to // - trackingId: A unique identifier for the account.This should be obtained from a previous call to
// CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the // CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
@ -32,9 +37,9 @@ type TestAccountService struct {
// Returns: // Returns:
// - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string. // - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. // - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil. // If no error occurs, this will be nil
func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) {
resp, err := http.Get(config.TrackStatusURL + trackingId) resp, err := http.Get(config.BalanceURL + trackingId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -44,12 +49,67 @@ func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackSt
if err != nil { if err != nil {
return nil, err return nil, err
} }
var trackResp models.TrackStatusResponse var trackResp models.TrackStatusResponse
err = json.Unmarshal(body, &trackResp) err = json.Unmarshal(body, &trackResp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &trackResp, nil return &trackResp, nil
}
func (as *AccountService) TrackAccountStatus(publicKey string) (*OKResponse, *ErrResponse) {
var errResponse ErrResponse
var okResponse OKResponse
var err error
// Construct the URL with the path parameter
url := fmt.Sprintf("%s/%s", config.TrackURL, publicKey)
req, err := http.NewRequest("GET", url, nil)
lash marked this conversation as resolved
Review

I think it is better and safer to use the net/url package for handling urls?

I will add it as a separate issue.

I think it is better and safer to use the `net/url` package for handling urls? I will add it as a separate issue.
Review
https://git.grassecon.net/urdt/ussd/issues/125
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GE-KEY", "xd")
// Send the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
Review

Check the HTTP response code first. If it is >= 400 (Bad request you can then unmarshal to an api.ErrResp.

Check the HTTP response code first. If it is >= 400 (Bad request you can then unmarshal to an api.ErrResp.
defer resp.Body.Close()
// Read the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
// Step 2: Unmarshal into the generic struct
err = json.Unmarshal([]byte(body), &apiResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
Outdated
Review

I would just use the literal 0 here

I would just use the literal `0` here

If there is no matching tracking id, result.otx will be null.

If there is no matching tracking id, result.otx will be null.
Outdated
Review

@kamikazechaser please elaborate is there a missing case?

@kamikazechaser please elaborate is there a missing case?
}
if apiResponse.Ok {
err = json.Unmarshal([]byte(body), &okResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
return &okResponse, nil
} else {
err := json.Unmarshal([]byte(body), &errResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
return nil, &errResponse
}
} }
// CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint. // CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
@ -79,22 +139,52 @@ func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceRespons
// If there is an error during the request or processing, this will be nil. // If there is an error during the request or processing, this will be nil.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. // - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil. // If no error occurs, this will be nil.
func (as *AccountService) CreateAccount() (*models.AccountResponse, error) { func (as *AccountService) CreateAccount() (*OKResponse, *ErrResponse) {
resp, err := http.Post(config.CreateAccountURL, "application/json", nil)
var errResponse ErrResponse
var okResponse OKResponse
var err error
// Create a new request
req, err := http.NewRequest("POST", config.CreateAccountURL, nil)
if err != nil { if err != nil {
return nil, err errResponse.Description = err.Error()
return nil, &errResponse
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GE-KEY", "xd")
resp, err := http.DefaultClient.Do(req)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err errResponse.Description = err.Error()
return nil, &errResponse
} }
var accountResp models.AccountResponse err = json.Unmarshal([]byte(body), &apiResponse)
err = json.Unmarshal(body, &accountResp)
if err != nil { if err != nil {
return nil, err return nil, &errResponse
}
if apiResponse.Ok {
err = json.Unmarshal([]byte(body), &okResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
return &okResponse, nil
} else {
err := json.Unmarshal([]byte(body), &errResponse)
if err != nil {
errResponse.Description = err.Error()
return nil, &errResponse
}
return nil, &errResponse
} }
return &accountResp, nil
} }
func (tas *TestAccountService) CreateAccount() (*models.AccountResponse, error) { func (tas *TestAccountService) CreateAccount() (*models.AccountResponse, error) {

View File

@ -0,0 +1,60 @@
package server
type (
OKResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
Result map[string]any `json:"result"`
}
ErrResponse struct {
Ok bool `json:"ok"`
Description string `json:"description"`
ErrCode string `json:"errorCode"`
}
TransferRequest struct {
From string `json:"from" validate:"required,eth_addr_checksum"`
To string `json:"to" validate:"required,eth_addr_checksum"`
TokenAddress string `json:"tokenAddress" validate:"required,eth_addr_checksum"`
Amount string `json:"amount" validate:"required,number,gt=0"`
}
PoolSwapRequest struct {
From string `json:"from" validate:"required,eth_addr_checksum"`
FromTokenAddress string `json:"fromTokenAddress" validate:"required,eth_addr_checksum"`
ToTokenAddress string `json:"toTokenAddress" validate:"required,eth_addr_checksum"`
PoolAddress string `json:"poolAddress" validate:"required,eth_addr_checksum"`
Amount string `json:"amount" validate:"required,number,gt=0"`
}
PoolDepositRequest struct {
From string `json:"from" validate:"required,eth_addr_checksum"`
TokenAddress string `json:"tokenAddress" validate:"required,eth_addr_checksum"`
PoolAddress string `json:"poolAddress" validate:"required,eth_addr_checksum"`
Amount string `json:"amount" validate:"required,number,gt=0"`
}
AccountAddressParam struct {
Address string `param:"address" validate:"required,eth_addr_checksum"`
}
TrackingIDParam struct {
TrackingID string `param:"trackingId" validate:"required,uuid"`
}
OTXByAccountRequest struct {
Address string `param:"address" validate:"required,eth_addr_checksum"`
PerPage int `query:"perPage" validate:"required,number,gt=0"`
Cursor int `query:"cursor" validate:"number"`
Next bool `query:"next"`
}
)
const (
ErrCodeInternalServerError = "E01"
ErrCodeInvalidJSON = "E02"
ErrCodeInvalidAPIKey = "E03"
ErrCodeValidationFailed = "E04"
ErrCodeAccountNotExists = "E05"
)

View File

@ -27,6 +27,8 @@ var (
logg = logging.NewVanilla().WithDomain("ussdmenuhandler") logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
translationDir = path.Join(scriptDir, "locale") translationDir = path.Join(scriptDir, "locale")
okResponse *server.OKResponse
errResponse *server.ErrResponse
) )
// FlagManager handles centralized flag management // FlagManager handles centralized flag management
@ -136,11 +138,16 @@ func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (r
} }
func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error { func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, res *resource.Result) error {
accountResp, err := h.accountService.CreateAccount() okResponse, errResponse := h.accountService.CreateAccount()
if errResponse != nil {
return nil
}
trackingId := okResponse.Result["trackingId"].(string)
publicKey := okResponse.Result["publicKey"].(string)
Outdated
Review

When is an error response "ok"?

When is an error response "ok"?
Outdated
Review

If unknown, perhaps @kamikazechaser can clear it up

If unknown, perhaps @kamikazechaser can clear it up

In the menu handler's context,we needed a way to decide which error from calling the CreateAccountService we could use to set the flag 'flag_api_call_error' and because an error could occur when calling the CreateAccount that's not associated with an api call,say maybe Unmarshaling,then checking if the Ok field is present and is false is what i considered as an api call failure.

In the menu handler's context,we needed a way to decide which error from calling the CreateAccountService we could use to set the flag 'flag_api_call_error' and because an error could occur when calling the CreateAccount that's not associated with an api call,say maybe Unmarshaling,then checking if the Ok field is present and is false is what i considered as an api call failure.
data := map[utils.DataTyp]string{ data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: accountResp.Result.TrackingId, utils.DATA_TRACKING_ID: trackingId,
utils.DATA_PUBLIC_KEY: accountResp.Result.PublicKey, utils.DATA_PUBLIC_KEY: publicKey,
utils.DATA_CUSTODIAL_ID: accountResp.Result.CustodialId.String(),
} }
for key, value := range data { for key, value := range data {
@ -152,7 +159,7 @@ func (h *Handlers) createAccountNoExist(ctx context.Context, sessionId string, r
} }
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created") flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
res.FlagSet = append(res.FlagSet, flag_account_created) res.FlagSet = append(res.FlagSet, flag_account_created)
return err return nil
} }
@ -191,7 +198,6 @@ func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resou
} }
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
accountPIN := string(input) accountPIN := string(input)
// Validate that the PIN is a 4-digit number // Validate that the PIN is a 4-digit number
if !isValidPIN(accountPIN) { if !isValidPIN(accountPIN) {
@ -368,7 +374,6 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
if len(input) == 4 { if len(input) == 4 {
yob := string(input) yob := string(input)
store := h.userdataStore store := h.userdataStore
@ -411,7 +416,6 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
gender := strings.Split(symbol, "_")[1] gender := strings.Split(symbol, "_")[1]
store := h.userdataStore store := h.userdataStore
err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender)) err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender))
@ -430,7 +434,6 @@ func (h *Handlers) SaveOfferings(ctx context.Context, sym string, input []byte)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
if len(input) > 0 { if len(input) > 0 {
offerings := string(input) offerings := string(input)
store := h.userdataStore store := h.userdataStore
@ -456,7 +459,6 @@ func (h *Handlers) ResetAllowUpdate(ctx context.Context, sym string, input []byt
// ResetAccountAuthorized resets the account authorization flag after a successful PIN entry. // ResetAccountAuthorized resets the account authorization flag after a successful PIN entry.
func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
@ -466,12 +468,10 @@ func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input
// CheckIdentifier retrieves the PublicKey from the JSON data file. // CheckIdentifier retrieves the PublicKey from the JSON data file.
func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) publicKey, _ := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
@ -485,12 +485,10 @@ func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte
func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error var err error
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
@ -542,28 +540,20 @@ func (h *Handlers) CheckAccountStatus(ctx context.Context, sym string, input []b
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
trackingId, err := store.ReadEntry(ctx, sessionId, utils.DATA_TRACKING_ID) publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY)
if err != nil { if err != nil {
return res, err return res, err
} }
okResponse, errResponse = h.accountService.TrackAccountStatus(string(publicKey))
accountStatus, err := h.accountService.CheckAccountStatus(string(trackingId)) if errResponse != nil {
if err != nil {
fmt.Println("Error checking account status:", err)
return res, err
}
if !accountStatus.Ok {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, err return res, err
} }
res.FlagReset = append(res.FlagReset, flag_api_error) res.FlagReset = append(res.FlagReset, flag_api_error)
status := accountStatus.Result.Transaction.Status isActive := okResponse.Result["active"].(bool)
if !ok {
err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)) return res, err
if err != nil {
return res, nil
} }
if accountStatus.Result.Transaction.Status == "SUCCESS" { if isActive {
res.FlagSet = append(res.FlagSet, flag_account_success) res.FlagSet = append(res.FlagSet, flag_account_success)
res.FlagReset = append(res.FlagReset, flag_account_pending) res.FlagReset = append(res.FlagReset, flag_account_pending)
} else { } else {

View File

@ -72,68 +72,75 @@ func TestCreateAccount(t *testing.T) {
if err != nil { if err != nil {
t.Logf(err.Error()) t.Logf(err.Error())
} }
// Create required mocks // Create required mocks
mockDataStore := new(mocks.MockUserDataStore) flag_account_created, err := fm.GetFlag("flag_account_created")
mockCreateAccountService := new(mocks.MockAccountService)
expectedResult := resource.Result{}
accountCreatedFlag, err := fm.GetFlag("flag_account_created")
if err != nil { if err != nil {
t.Logf(err.Error()) t.Logf(err.Error())
} }
expectedResult.FlagSet = append(expectedResult.FlagSet, accountCreatedFlag)
// Define session ID and mock data // Define session ID and mock data
sessionId := "session123" sessionId := "session123"
typ := utils.DATA_ACCOUNT_CREATED notFoundErr := db.ErrNotFound{}
fakeError := db.ErrNotFound{}
// Create context with session ID
ctx := context.WithValue(context.Background(), "SessionId", sessionId) ctx := context.WithValue(context.Background(), "SessionId", sessionId)
// Define expected interactions with the mock tests := []struct {
mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte("123"), fakeError) name string
expectedAccountResp := &models.AccountResponse{ serverResponse *server.OKResponse
Ok: true, expectedResult resource.Result
Result: struct { }{
CustodialId json.Number `json:"custodialId"` {
PublicKey string `json:"publicKey"` name: "Test account creation success",
TrackingId string `json:"trackingId"` serverResponse: &server.OKResponse{
}{ Ok: true,
CustodialId: "12", Description: "Account creation successed",
PublicKey: "0x8E0XSCSVA", Result: map[string]any{
TrackingId: "d95a7e83-196c-4fd0-866fSGAGA", "trackingId": "1234567890",
"publicKey": "1235QERYU",
},
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_account_created},
},
}, },
} }
mockCreateAccountService.On("CreateAccount").Return(expectedAccountResp, nil) for _, tt := range tests {
data := map[utils.DataTyp]string{ t.Run(tt.name, func(t *testing.T) {
utils.DATA_TRACKING_ID: expectedAccountResp.Result.TrackingId,
utils.DATA_PUBLIC_KEY: expectedAccountResp.Result.PublicKey, mockDataStore := new(mocks.MockUserDataStore)
utils.DATA_CUSTODIAL_ID: expectedAccountResp.Result.CustodialId.String(), mockCreateAccountService := new(mocks.MockAccountService)
// Create a Handlers instance with the mock data store
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
data := map[utils.DataTyp]string{
utils.DATA_TRACKING_ID: tt.serverResponse.Result["trackingId"].(string),
utils.DATA_PUBLIC_KEY: tt.serverResponse.Result["publicKey"].(string),
}
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_ACCOUNT_CREATED).Return([]byte(""), notFoundErr)
mockCreateAccountService.On("CreateAccount").Return(tt.serverResponse, nil)
for key, value := range data {
mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil)
}
// Call the method you want to test
res, err := h.CreateAccount(ctx, "create_account", []byte("some-input"))
// Assert that no errors occurred
assert.NoError(t, err)
// Assert that the account created flag has been set to the result
assert.Equal(t, res, tt.expectedResult, "Expected result should be equal to the actual result")
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
})
} }
for key, value := range data {
mockDataStore.On("WriteEntry", ctx, sessionId, key, []byte(value)).Return(nil)
}
// Create a Handlers instance with the mock data store
h := &Handlers{
userdataStore: mockDataStore,
accountService: mockCreateAccountService,
flagManager: fm.parser,
}
// Call the method you want to test
res, err := h.CreateAccount(ctx, "create_account", []byte("some-input"))
// Assert that no errors occurred
assert.NoError(t, err)
//Assert that the account created flag has been set to the result
assert.Equal(t, res, expectedResult, "Expected result should be equal to the actual result")
// Assert that expectations were met
mockDataStore.AssertExpectations(t)
} }
func TestWithPersister(t *testing.T) { func TestWithPersister(t *testing.T) {
@ -1066,12 +1073,20 @@ func TestCheckAccountStatus(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input []byte input []byte
serverResponse *server.OKResponse
response *models.TrackStatusResponse response *models.TrackStatusResponse
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test when account status is Success", name: "Test when account is on the Sarafu network",
input: []byte("TrackingId1234"), input: []byte("TrackingId1234"),
serverResponse: &server.OKResponse{
Ok: true,
Description: "Account creation successed",
Result: map[string]any{
"active": true,
},
},
response: &models.TrackStatusResponse{ response: &models.TrackStatusResponse{
Ok: true, Ok: true,
Result: struct { Result: struct {
@ -1098,17 +1113,7 @@ func TestCheckAccountStatus(t *testing.T) {
}, },
}, },
{ {
name: "Test when fetching account status is not Success", name: "Test when the account is not yet on the sarafu network",
input: []byte("TrackingId1234"),
response: &models.TrackStatusResponse{
Ok: false,
},
expectedResult: resource.Result{
FlagSet: []uint32{flag_api_error},
},
},
{
name: "Test when checking account status api call is a SUCCESS but an account is not yet ready",
input: []byte("TrackingId1234"), input: []byte("TrackingId1234"),
response: &models.TrackStatusResponse{ response: &models.TrackStatusResponse{
Ok: true, Ok: true,
@ -1123,13 +1128,20 @@ func TestCheckAccountStatus(t *testing.T) {
}{ }{
Transaction: models.Transaction{ Transaction: models.Transaction{
CreatedAt: time.Now(), CreatedAt: time.Now(),
Status: "IN_NETWORK", Status: "SUCCESS",
TransferValue: json.Number("0.5"), TransferValue: json.Number("0.5"),
TxHash: "0x123abc456def", TxHash: "0x123abc456def",
TxType: "transfer", TxType: "transfer",
}, },
}, },
}, },
serverResponse: &server.OKResponse{
Ok: true,
Description: "Account creation successed",
Result: map[string]any{
"active": false,
},
},
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagSet: []uint32{flag_account_pending}, FlagSet: []uint32{flag_account_pending},
FlagReset: []uint32{flag_api_error, flag_account_success}, FlagReset: []uint32{flag_api_error, flag_account_success},
@ -1149,9 +1161,10 @@ func TestCheckAccountStatus(t *testing.T) {
status := tt.response.Result.Transaction.Status status := tt.response.Result.Transaction.Status
// Define expected interactions with the mock // Define expected interactions with the mock
mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_TRACKING_ID).Return(tt.input, nil) mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.input, nil)
mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.response, nil) mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.response, nil)
mockCreateAccountService.On("TrackAccountStatus", string(tt.input)).Return(tt.serverResponse, nil)
mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)).Return(nil).Maybe() mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)).Return(nil).Maybe()
// Call the method under test // Call the method under test

View File

@ -1,6 +1,7 @@
package mocks package mocks
import ( import (
"git.grassecon.net/urdt/ussd/internal/handlers/server"
"git.grassecon.net/urdt/ussd/internal/models" "git.grassecon.net/urdt/ussd/internal/models"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
@ -10,9 +11,19 @@ type MockAccountService struct {
mock.Mock mock.Mock
} }
func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) { func (m *MockAccountService) CreateAccount() (*server.OKResponse, *server.ErrResponse) {
args := m.Called() args := m.Called()
return args.Get(0).(*models.AccountResponse), args.Error(1) okResponse, ok := args.Get(0).(*server.OKResponse)
errResponse, err := args.Get(1).(*server.ErrResponse)
if ok {
return okResponse, nil
}
if err {
return nil, errResponse
}
return nil, nil
} }
func (m *MockAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) { func (m *MockAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) {
@ -24,3 +35,16 @@ func (m *MockAccountService) CheckAccountStatus(trackingId string) (*models.Trac
args := m.Called(trackingId) args := m.Called(trackingId)
return args.Get(0).(*models.TrackStatusResponse), args.Error(1) return args.Get(0).(*models.TrackStatusResponse), args.Error(1)
} }
func (m *MockAccountService) TrackAccountStatus(publicKey string) (*server.OKResponse, *server.ErrResponse) {
args := m.Called(publicKey)
okResponse, ok := args.Get(0).(*server.OKResponse)
errResponse, err := args.Get(1).(*server.ErrResponse)
if ok {
return okResponse, nil
}
if err {
return nil, errResponse
}
return nil, nil
}

View File

@ -1,15 +1,10 @@
package models package models
import (
"encoding/json"
)
type AccountResponse struct { type AccountResponse struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
Result struct { Description string `json:"description"` // Include the description field
CustodialId json.Number `json:"custodialId"` Result struct {
PublicKey string `json:"publicKey"` PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"` TrackingId string `json:"trackingId"`
} `json:"result"` } `json:"result"`
} }

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"time" "time"
) )
type Transaction struct { type Transaction struct {
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
Status string `json:"status"` Status string `json:"status"`