diff --git a/config/config.go b/config/config.go index 0571503..a035d24 100644 --- a/config/config.go +++ b/config/config.go @@ -1,10 +1,7 @@ package config - - const ( - CreateAccountURL = "https://custodial.sarafu.africa/api/account/create" - TrackStatusURL = "https://custodial.sarafu.africa/api/track/" - BalanceURL = "https://custodial.sarafu.africa/api/account/status/" + CreateAccountURL = "http://localhost:5003/api/v2/account/create" + BalanceURL = "https://custodial.sarafu.africa/api/account/status/" + TrackURL = "http://localhost:5003/api/v2/account/status" ) - diff --git a/internal/handlers/server/accountservice.go b/internal/handlers/server/accountservice.go index 5b71e6f..451c661 100644 --- a/internal/handlers/server/accountservice.go +++ b/internal/handlers/server/accountservice.go @@ -2,6 +2,7 @@ package server import ( "encoding/json" + "fmt" "io" "net/http" "time" @@ -10,10 +11,16 @@ import ( "git.grassecon.net/urdt/ussd/internal/models" ) +var apiResponse struct { + Ok bool `json:"ok"` + Description string `json:"description"` +} + type AccountServiceInterface interface { CheckBalance(publicKey string) (*models.BalanceResponse, error) - CreateAccount() (*models.AccountResponse, error) + CreateAccount() (*OKResponse, *ErrResponse) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) + TrackAccountStatus(publicKey string) (*OKResponse, *ErrResponse) } type AccountService struct { @@ -22,8 +29,6 @@ type AccountService struct { type TestAccountService struct { } -// CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID. -// // Parameters: // - 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 @@ -32,9 +37,9 @@ type TestAccountService struct { // 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. // - 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) { - resp, err := http.Get(config.TrackStatusURL + trackingId) + resp, err := http.Get(config.BalanceURL + trackingId) if err != nil { return nil, err } @@ -44,12 +49,67 @@ func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackSt if err != nil { return nil, err } + var trackResp models.TrackStatusResponse err = json.Unmarshal(body, &trackResp) if err != nil { return nil, err } 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) + 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 + } + 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 + } + 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. @@ -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. // - 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. -func (as *AccountService) CreateAccount() (*models.AccountResponse, error) { - resp, err := http.Post(config.CreateAccountURL, "application/json", nil) +func (as *AccountService) CreateAccount() (*OKResponse, *ErrResponse) { + + var errResponse ErrResponse + var okResponse OKResponse + var err error + + // Create a new request + req, err := http.NewRequest("POST", config.CreateAccountURL, 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() + body, err := io.ReadAll(resp.Body) if err != nil { - return nil, err + errResponse.Description = err.Error() + return nil, &errResponse } - var accountResp models.AccountResponse - err = json.Unmarshal(body, &accountResp) + err = json.Unmarshal([]byte(body), &apiResponse) 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) { diff --git a/internal/handlers/server/api.go b/internal/handlers/server/api.go new file mode 100644 index 0000000..646458c --- /dev/null +++ b/internal/handlers/server/api.go @@ -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" +) diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 36d1ad5..3efcbb1 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -27,6 +27,8 @@ var ( logg = logging.NewVanilla().WithDomain("ussdmenuhandler") scriptDir = path.Join("services", "registration") translationDir = path.Join(scriptDir, "locale") + okResponse *server.OKResponse + errResponse *server.ErrResponse ) // 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 { - 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) + data := map[utils.DataTyp]string{ - utils.DATA_TRACKING_ID: accountResp.Result.TrackingId, - utils.DATA_PUBLIC_KEY: accountResp.Result.PublicKey, - utils.DATA_CUSTODIAL_ID: accountResp.Result.CustodialId.String(), + utils.DATA_TRACKING_ID: trackingId, + utils.DATA_PUBLIC_KEY: publicKey, } 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") 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") - accountPIN := string(input) // Validate that the PIN is a 4-digit number if !isValidPIN(accountPIN) { @@ -368,7 +374,6 @@ func (h *Handlers) SaveYob(ctx context.Context, sym string, input []byte) (resou if !ok { return res, fmt.Errorf("missing session") } - if len(input) == 4 { yob := string(input) store := h.userdataStore @@ -411,7 +416,6 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re if !ok { return res, fmt.Errorf("missing session") } - gender := strings.Split(symbol, "_")[1] store := h.userdataStore 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 { return res, fmt.Errorf("missing session") } - if len(input) > 0 { offerings := string(input) 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. func (h *Handlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - flag_account_authorized, _ := h.flagManager.GetFlag("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. func (h *Handlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } - store := h.userdataStore 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) { var res resource.Result var err error - sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } - flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") 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") } 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 { return res, err } - - accountStatus, err := h.accountService.CheckAccountStatus(string(trackingId)) - if err != nil { - fmt.Println("Error checking account status:", err) - return res, err - } - if !accountStatus.Ok { - res.FlagSet = append(res.FlagSet, flag_api_error) + okResponse, errResponse = h.accountService.TrackAccountStatus(string(publicKey)) + if errResponse != nil { return res, err } res.FlagReset = append(res.FlagReset, flag_api_error) - status := accountStatus.Result.Transaction.Status - - err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)) - if err != nil { - return res, nil + isActive := okResponse.Result["active"].(bool) + if !ok { + return res, err } - if accountStatus.Result.Transaction.Status == "SUCCESS" { + if isActive { res.FlagSet = append(res.FlagSet, flag_account_success) res.FlagReset = append(res.FlagReset, flag_account_pending) } else { diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index 38c468c..de1a481 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -72,68 +72,75 @@ func TestCreateAccount(t *testing.T) { if err != nil { t.Logf(err.Error()) } - // Create required mocks - mockDataStore := new(mocks.MockUserDataStore) - mockCreateAccountService := new(mocks.MockAccountService) - expectedResult := resource.Result{} - accountCreatedFlag, err := fm.GetFlag("flag_account_created") - + flag_account_created, err := fm.GetFlag("flag_account_created") if err != nil { t.Logf(err.Error()) } - expectedResult.FlagSet = append(expectedResult.FlagSet, accountCreatedFlag) // Define session ID and mock data sessionId := "session123" - typ := utils.DATA_ACCOUNT_CREATED - fakeError := db.ErrNotFound{} - // Create context with session ID + notFoundErr := db.ErrNotFound{} ctx := context.WithValue(context.Background(), "SessionId", sessionId) - // Define expected interactions with the mock - mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte("123"), fakeError) - expectedAccountResp := &models.AccountResponse{ - Ok: true, - Result: struct { - CustodialId json.Number `json:"custodialId"` - PublicKey string `json:"publicKey"` - TrackingId string `json:"trackingId"` - }{ - CustodialId: "12", - PublicKey: "0x8E0XSCSVA", - TrackingId: "d95a7e83-196c-4fd0-866fSGAGA", + tests := []struct { + name string + serverResponse *server.OKResponse + expectedResult resource.Result + }{ + { + name: "Test account creation success", + serverResponse: &server.OKResponse{ + Ok: true, + Description: "Account creation successed", + Result: map[string]any{ + "trackingId": "1234567890", + "publicKey": "1235QERYU", + }, + }, + expectedResult: resource.Result{ + FlagSet: []uint32{flag_account_created}, + }, }, } - mockCreateAccountService.On("CreateAccount").Return(expectedAccountResp, nil) - data := map[utils.DataTyp]string{ - utils.DATA_TRACKING_ID: expectedAccountResp.Result.TrackingId, - utils.DATA_PUBLIC_KEY: expectedAccountResp.Result.PublicKey, - utils.DATA_CUSTODIAL_ID: expectedAccountResp.Result.CustodialId.String(), + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + mockDataStore := new(mocks.MockUserDataStore) + 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) { @@ -1066,12 +1073,20 @@ func TestCheckAccountStatus(t *testing.T) { tests := []struct { name string input []byte + serverResponse *server.OKResponse response *models.TrackStatusResponse expectedResult resource.Result }{ { - name: "Test when account status is Success", + name: "Test when account is on the Sarafu network", input: []byte("TrackingId1234"), + serverResponse: &server.OKResponse{ + Ok: true, + Description: "Account creation successed", + Result: map[string]any{ + "active": true, + }, + }, response: &models.TrackStatusResponse{ Ok: true, Result: struct { @@ -1098,17 +1113,7 @@ func TestCheckAccountStatus(t *testing.T) { }, }, { - name: "Test when fetching account status is not Success", - 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", + name: "Test when the account is not yet on the sarafu network", input: []byte("TrackingId1234"), response: &models.TrackStatusResponse{ Ok: true, @@ -1123,13 +1128,20 @@ func TestCheckAccountStatus(t *testing.T) { }{ Transaction: models.Transaction{ CreatedAt: time.Now(), - Status: "IN_NETWORK", + Status: "SUCCESS", TransferValue: json.Number("0.5"), TxHash: "0x123abc456def", TxType: "transfer", }, }, }, + serverResponse: &server.OKResponse{ + Ok: true, + Description: "Account creation successed", + Result: map[string]any{ + "active": false, + }, + }, expectedResult: resource.Result{ FlagSet: []uint32{flag_account_pending}, FlagReset: []uint32{flag_api_error, flag_account_success}, @@ -1149,9 +1161,10 @@ func TestCheckAccountStatus(t *testing.T) { status := tt.response.Result.Transaction.Status // 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("TrackAccountStatus", string(tt.input)).Return(tt.serverResponse, nil) mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(status)).Return(nil).Maybe() // Call the method under test diff --git a/internal/mocks/servicemock.go b/internal/mocks/servicemock.go index d828045..7796b77 100644 --- a/internal/mocks/servicemock.go +++ b/internal/mocks/servicemock.go @@ -1,6 +1,7 @@ package mocks import ( + "git.grassecon.net/urdt/ussd/internal/handlers/server" "git.grassecon.net/urdt/ussd/internal/models" "github.com/stretchr/testify/mock" ) @@ -10,9 +11,19 @@ type MockAccountService struct { mock.Mock } -func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) { +func (m *MockAccountService) CreateAccount() (*server.OKResponse, *server.ErrResponse) { 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) { @@ -23,4 +34,17 @@ func (m *MockAccountService) CheckBalance(publicKey string) (*models.BalanceResp func (m *MockAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { args := m.Called(trackingId) return args.Get(0).(*models.TrackStatusResponse), args.Error(1) -} \ No newline at end of file +} + +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 +} diff --git a/internal/models/accountresponse.go b/internal/models/accountresponse.go index 1422a20..efcc30b 100644 --- a/internal/models/accountresponse.go +++ b/internal/models/accountresponse.go @@ -1,15 +1,10 @@ package models -import ( - "encoding/json" - -) - type AccountResponse struct { - Ok bool `json:"ok"` - Result struct { - CustodialId json.Number `json:"custodialId"` - PublicKey string `json:"publicKey"` - TrackingId string `json:"trackingId"` + Ok bool `json:"ok"` + Description string `json:"description"` // Include the description field + Result struct { + PublicKey string `json:"publicKey"` + TrackingId string `json:"trackingId"` } `json:"result"` -} \ No newline at end of file +} diff --git a/internal/models/trackstatusresponse.go b/internal/models/trackstatusresponse.go index 1629a7c..6b96d90 100644 --- a/internal/models/trackstatusresponse.go +++ b/internal/models/trackstatusresponse.go @@ -4,7 +4,6 @@ import ( "encoding/json" "time" ) - type Transaction struct { CreatedAt time.Time `json:"createdAt"` Status string `json:"status"`