diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 805dcff..bd710e8 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -117,17 +117,14 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - sym, _ = h.st.Where() + symbol, _ := h.st.Where() + code := strings.Split(symbol, "_")[1] - switch sym { - case "set_default": - res.FlagSet = append(res.FlagSet, state.FLAG_LANG) - res.Content = "eng" - case "set_swa": - res.FlagSet = append(res.FlagSet, state.FLAG_LANG) - res.Content = "swa" - default: + if !utils.IsValidISO639(code) { + return res, nil } + res.FlagSet = append(res.FlagSet, state.FLAG_LANG) + res.Content = code languageSetFlag, err := h.flagManager.GetFlag("flag_language_set") if err != nil { @@ -282,26 +279,11 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt // SetResetSingleEdit sets and resets flags to allow gradual editing of profile information. func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - - menuOption := string(input) - - flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") - flag_single_edit, _ := h.flagManager.GetFlag("flag_single_edit") - - switch menuOption { - case "2": - res.FlagReset = append(res.FlagReset, flag_allow_update) - res.FlagSet = append(res.FlagSet, flag_single_edit) - case "3": - res.FlagReset = append(res.FlagReset, flag_allow_update) - res.FlagSet = append(res.FlagSet, flag_single_edit) - case "4": - res.FlagReset = append(res.FlagReset, flag_allow_update) - res.FlagSet = append(res.FlagSet, flag_single_edit) - default: - res.FlagReset = append(res.FlagReset, flag_single_edit) + flag_single_edit, err := h.flagManager.GetFlag("flag_single_edit") + if err != nil { + return res, err } - + res.FlagReset = append(res.FlagReset, flag_single_edit) return res, nil } @@ -319,8 +301,6 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res if !ok { return res, fmt.Errorf("missing session") } - - //AccountPin, _ := utils.ReadEntry(ctx, h.userdataStore, sessionId, utils.DATA_ACCOUNT_PIN) store := h.userdataStore AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN) if err != nil { @@ -357,6 +337,12 @@ func (h *Handlers) SaveFirstname(ctx context.Context, sym string, input []byte) return res, fmt.Errorf("missing session") } + flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") + flag_single_edit, _ := h.flagManager.GetFlag("flag_single_edit") + + res.FlagReset = append(res.FlagReset, flag_allow_update) + res.FlagSet = append(res.FlagSet, flag_single_edit) + if len(input) > 0 { firstName := string(input) store := h.userdataStore @@ -377,7 +363,6 @@ func (h *Handlers) SaveFamilyname(ctx context.Context, sym string, input []byte) if !ok { return res, fmt.Errorf("missing session") } - if len(input) > 0 { familyName := string(input) store := h.userdataStore @@ -400,7 +385,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 @@ -421,7 +405,6 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) ( if !ok { return res, fmt.Errorf("missing session") } - if len(input) > 0 { location := string(input) store := h.userdataStore @@ -442,16 +425,29 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re if !ok { return res, fmt.Errorf("missing session") } - + code := codeFromCtx(ctx) if len(input) > 0 { gender := string(input) switch gender { case "1": - gender = "Male" + if code == "swa" { + gender = "Mwanaume" + } else { + gender = "Male" + } case "2": - gender = "Female" + if code == "swa" { + gender = "Mwanamke" + } else { + gender = "Female" + } case "3": - gender = "Unspecified" + if code == "swa" { + gender = "Haijabainishwa" + } else { + gender = "Unspecified" + } + } store := h.userdataStore err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender)) @@ -640,7 +636,6 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) ( return res, nil } - // VerifyYob verifies the length of the given input func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result @@ -988,13 +983,20 @@ func (h *Handlers) InitiateTransaction(ctx context.Context, sym string, input [] // GetProfileInfo retrieves and formats the profile information of a user from a Gdbm backed storage. func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result + var defaultValue string sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") } + code := h.st.Language.Code + // Default value when an entry is not found - defaultValue := "Not Provided" + if code == "swa" { + defaultValue = "Haipo" + } else { + defaultValue = "Not Provided" + } // Helper function to handle nil byte slices and convert them to string getEntryOrDefault := func(entry []byte, err error) string { @@ -1031,12 +1033,22 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) return res, fmt.Errorf("invalid year of birth: %v", err) } } - - // Format the result - res.Content = fmt.Sprintf( - "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", - name, gender, age, location, offerings, - ) - + switch code { + case "eng": + res.Content = fmt.Sprintf( + "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", + name, gender, age, location, offerings, + ) + case "swa": + res.Content = fmt.Sprintf( + "Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n", + name, gender, age, location, offerings, + ) + default: + res.Content = fmt.Sprintf( + "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", + name, gender, age, location, offerings, + ) + } return res, nil } diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index d0367f0..231a29d 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -9,6 +9,7 @@ import ( "testing" "git.defalsify.org/vise.git/db" + "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/state" "git.grassecon.net/urdt/ussd/internal/mocks" @@ -16,6 +17,7 @@ import ( "git.grassecon.net/urdt/ussd/internal/utils" "github.com/alecthomas/assert/v2" testdataloader "github.com/peteole/testdata-loader" + "github.com/stretchr/testify/require" ) var ( @@ -95,6 +97,11 @@ func TestCreateAccount(t *testing.T) { } func TestSaveFirstname(t *testing.T) { + + fm, err := NewFlagManager(flagsPath) + if err != nil { + t.Fatal(err) + } // Create a new instance of MockMyDataStore mockStore := new(mocks.MockUserDataStore) @@ -103,12 +110,16 @@ func TestSaveFirstname(t *testing.T) { firstName := "John" ctx := context.WithValue(context.Background(), "SessionId", sessionId) + flag_allow_update, _ := fm.parser.GetFlag("flag_allow_update") + flag_single_edit, _ := fm.parser.GetFlag("flag_single_edit") + // Set up the expected behavior of the mock mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_FIRST_NAME, []byte(firstName)).Return(nil) // Create the Handlers instance with the mock store h := &Handlers{ userdataStore: mockStore, + flagManager: fm.parser, } // Call the method @@ -116,7 +127,10 @@ func TestSaveFirstname(t *testing.T) { // Assert results assert.NoError(t, err) - assert.Equal(t, resource.Result{}, res) + assert.Equal(t, resource.Result{ + FlagSet: []uint32{flag_single_edit}, + FlagReset: []uint32{flag_allow_update}, + }, res) // Assert all expectations were met mockStore.AssertExpectations(t) @@ -349,7 +363,7 @@ func TestSaveGender(t *testing.T) { } // Call the method - _, err := h.SaveGender(ctx, "someSym", tt.input) + _, err := h.SaveGender(ctx, "save_gender", tt.input) // Assert no error assert.NoError(t, err) @@ -538,13 +552,15 @@ func TestSetLanguage(t *testing.T) { } // Define test cases tests := []struct { - name string - execPath []string - expectedResult resource.Result + name string + execPath []string + expectedResult resource.Result + symbol string }{ { name: "Set Default Language (English)", - execPath: []string{"set_default"}, + execPath: []string{"set_eng"}, + symbol: "set_eng", expectedResult: resource.Result{ FlagSet: []uint32{state.FLAG_LANG, 8}, Content: "eng", @@ -552,19 +568,13 @@ func TestSetLanguage(t *testing.T) { }, { name: "Set Swahili Language", + symbol: "set_swa", execPath: []string{"set_swa"}, expectedResult: resource.Result{ FlagSet: []uint32{state.FLAG_LANG, 8}, Content: "swa", }, }, - { - name: "Unhandled path", - execPath: []string{""}, - expectedResult: resource.Result{ - FlagSet: []uint32{8}, - }, - }, } for _, tt := range tests { @@ -580,7 +590,7 @@ func TestSetLanguage(t *testing.T) { } // Call the method - res, err := h.SetLanguage(context.Background(), "set_language", nil) + res, err := h.SetLanguage(context.Background(), tt.symbol, nil) if err != nil { t.Error(err) @@ -596,7 +606,7 @@ func TestSetLanguage(t *testing.T) { func TestSetResetSingleEdit(t *testing.T) { fm, err := NewFlagManager(flagsPath) - flag_allow_update, _ := fm.parser.GetFlag("flag_allow_update") + //flag_allow_update, _ := fm.parser.GetFlag("flag_allow_update") flag_single_edit, _ := fm.parser.GetFlag("flag_single_edit") if err != nil { @@ -608,30 +618,30 @@ func TestSetResetSingleEdit(t *testing.T) { input []byte expectedResult resource.Result }{ - { - name: "Set single Edit", - input: []byte("2"), - expectedResult: resource.Result{ - FlagSet: []uint32{flag_single_edit}, - FlagReset: []uint32{flag_allow_update}, - }, - }, - { - name: "Set single Edit", - input: []byte("3"), - expectedResult: resource.Result{ - FlagSet: []uint32{flag_single_edit}, - FlagReset: []uint32{flag_allow_update}, - }, - }, - { - name: "Set single edit", - input: []byte("4"), - expectedResult: resource.Result{ - FlagReset: []uint32{flag_allow_update}, - FlagSet: []uint32{flag_single_edit}, - }, - }, + // { + // name: "Set single Edit", + // input: []byte("2"), + // expectedResult: resource.Result{ + // FlagSet: []uint32{flag_single_edit}, + // FlagReset: []uint32{flag_allow_update}, + // }, + // }, + // { + // name: "Set single Edit", + // input: []byte("3"), + // expectedResult: resource.Result{ + // FlagSet: []uint32{flag_single_edit}, + // FlagReset: []uint32{flag_allow_update}, + // }, + // }, + // { + // name: "Set single edit", + // input: []byte("4"), + // expectedResult: resource.Result{ + // FlagReset: []uint32{flag_allow_update}, + // FlagSet: []uint32{flag_single_edit}, + // }, + // }, { name: "No single edit set", input: []byte("1"), @@ -1101,18 +1111,26 @@ func TestCheckAccountStatus(t *testing.T) { FlagReset: []uint32{flag_account_pending}, }, }, + { + name: "Test when account status is not a success", + input: []byte("TrackingId12"), + status: "REVERTED", + expectedResult: resource.Result{ + FlagSet: []uint32{flag_account_success}, + FlagReset: []uint32{flag_account_pending}, + }, + }, } typ := utils.DATA_TRACKING_ID for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.status, nil) + mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(tt.status)).Return(nil).Maybe() // Define expected interactions with the mock mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return(tt.input, nil) - mockCreateAccountService.On("CheckAccountStatus", string(tt.input)).Return(tt.status, nil) - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_STATUS, []byte(tt.status)).Return(nil) - // Call the method under test res, _ := h.CheckAccountStatus(ctx, "check_status", tt.input) @@ -1480,7 +1498,7 @@ func TestValidateAmount(t *testing.T) { if err != nil { t.Logf(err.Error()) } - //flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount") + flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount") mockDataStore := new(mocks.MockUserDataStore) mockCreateAccountService := new(mocks.MockAccountService) @@ -1509,26 +1527,26 @@ func TestValidateAmount(t *testing.T) { Content: "0.001", }, }, - // { - // name: "Test with amount larger than balance", - // input: []byte("0.02"), - // balance: "0.003 CELO", - // publicKey: []byte("0xrqeqrequuq"), - // expectedResult: resource.Result{ - // FlagSet: []uint32{flag_invalid_amount}, - // Content: "0.02", - // }, - // }, - // { - // name: "Test with invalid amount", - // input: []byte("0.02ms"), - // balance: "0.003 CELO", - // publicKey: []byte("0xrqeqrequuq"), - // expectedResult: resource.Result{ - // FlagSet: []uint32{flag_invalid_amount}, - // Content: "0.02ms", - // }, - // }, + { + name: "Test with amount larger than balance", + input: []byte("0.02"), + balance: "0.003 CELO", + publicKey: []byte("0xrqeqrequuq"), + expectedResult: resource.Result{ + FlagSet: []uint32{flag_invalid_amount}, + Content: "0.02", + }, + }, + { + name: "Test with invalid amount", + input: []byte("0.02ms"), + balance: "0.003 CELO", + publicKey: []byte("0xrqeqrequuq"), + expectedResult: resource.Result{ + FlagSet: []uint32{flag_invalid_amount}, + Content: "0.02ms", + }, + }, } for _, tt := range tests { @@ -1536,7 +1554,7 @@ func TestValidateAmount(t *testing.T) { mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil) mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balance, nil) - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil) + mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe() // Call the method under test res, _ := h.ValidateAmount(ctx, "test_validate_amount", tt.input) @@ -1630,7 +1648,6 @@ func TestCheckBalance(t *testing.T) { h := &Handlers{ userdataStore: mockDataStore, accountService: mockCreateAccountService, - //flagManager: fm.parser, } //mock call operations mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return([]byte(publicKey), nil) @@ -1648,22 +1665,51 @@ func TestGetProfile(t *testing.T) { mockDataStore := new(mocks.MockUserDataStore) mockCreateAccountService := new(mocks.MockAccountService) + mockState := state.NewState(16) + h := &Handlers{ userdataStore: mockDataStore, accountService: mockCreateAccountService, + st: mockState, } ctx := context.WithValue(context.Background(), "SessionId", sessionId) tests := []struct { - name string - keys []utils.DataTyp - profileInfo []string - result resource.Result + name string + languageCode string + keys []utils.DataTyp + profileInfo []string + result resource.Result }{ { - name: "Test with full profile information", - keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB}, - profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"}, + name: "Test with full profile information in eng", + keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB}, + profileInfo: []string{"Doee", "John", "Male", "Bananas", "Kilifi", "1976"}, + languageCode: "eng", + result: resource.Result{ + Content: fmt.Sprintf( + "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", + "John Doee", "Male", "48", "Kilifi", "Bananas", + ), + }, + }, + { + name: "Test with with profile information in swa ", + keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB}, + profileInfo: []string{"Doee", "John", "Jinsia", "Bananas", "Kilifi", "1976"}, + languageCode: "swa", + result: resource.Result{ + Content: fmt.Sprintf( + "Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n", + "John Doee", "Male", "48", "Kilifi", "Bananas", + ), + }, + }, + { + name: "Test with with profile information with language that is not yet supported", + keys: []utils.DataTyp{utils.DATA_FAMILY_NAME, utils.DATA_FIRST_NAME, utils.DATA_GENDER, utils.DATA_OFFERINGS, utils.DATA_LOCATION, utils.DATA_YOB}, + profileInfo: []string{"Doee", "John", "Jinsia", "Bananas", "Kilifi", "1976"}, + languageCode: "nor", result: resource.Result{ Content: fmt.Sprintf( "Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", @@ -1677,6 +1723,13 @@ func TestGetProfile(t *testing.T) { for index, key := range tt.keys { mockDataStore.On("ReadEntry", ctx, sessionId, key).Return([]byte(tt.profileInfo[index]), nil) } + + err := mockState.SetLanguage(tt.languageCode) + if err != nil { + t.Fail() + } + h.st = mockState + res, _ := h.GetProfileInfo(ctx, "get_profile_info", []byte("")) // Assert that expectations were met @@ -1784,6 +1837,25 @@ func TestSaveTemporaryPIn(t *testing.T) { mockStore.AssertExpectations(t) } +func TestWithPersister(t *testing.T) { + // Test case: Setting a persister + h := &Handlers{} + p := &persist.Persister{} + + result := h.WithPersister(p) + + assert.Equal(t, p, h.pe, "The persister should be set correctly.") + assert.Equal(t, h, result, "The returned handler should be the same instance.") +} + +func TestWithPersister_PanicWhenAlreadySet(t *testing.T) { + // Test case: Panic on multiple calls + h := &Handlers{pe: &persist.Persister{}} + require.Panics(t, func() { + h.WithPersister(&persist.Persister{}) + }, "Should panic when trying to set a persister again.") +} + func TestConfirmPin(t *testing.T) { sessionId := "session123" diff --git a/internal/utils/isocodes.go b/internal/utils/isocodes.go new file mode 100644 index 0000000..3bdfbeb --- /dev/null +++ b/internal/utils/isocodes.go @@ -0,0 +1,11 @@ +package utils + +var isoCodes = map[string]bool{ + "eng": true, // English + "swa": true, // Swahili + +} + +func IsValidISO639(code string) bool { + return isoCodes[code] +} diff --git a/services/registration/enter_location.vis b/services/registration/enter_location.vis index 00bed3d..4d66361 100644 --- a/services/registration/enter_location.vis +++ b/services/registration/enter_location.vis @@ -5,5 +5,5 @@ MOUT back 0 HALT INCMP _ 0 LOAD save_location 0 -CATCH pin_entry flag_single_edit 1 +CATCH pin_entry flag_single_edit 0 INCMP enter_offerings * diff --git a/services/registration/enter_yob.vis b/services/registration/enter_yob.vis index 3b27846..1b2e519 100644 --- a/services/registration/enter_yob.vis +++ b/services/registration/enter_yob.vis @@ -5,5 +5,5 @@ HALT INCMP _ 0 LOAD verify_yob 8 LOAD save_yob 0 -CATCH pin_entry flag_single_edit 1 +CATCH pin_entry flag_single_edit 0 INCMP enter_location * diff --git a/services/registration/select_gender.vis b/services/registration/select_gender.vis index dd354fc..d7b7376 100644 --- a/services/registration/select_gender.vis +++ b/services/registration/select_gender.vis @@ -6,7 +6,7 @@ MOUT unspecified 3 MOUT back 0 HALT LOAD save_gender 0 -CATCH pin_entry flag_single_edit 1 +CATCH pin_entry flag_single_edit 0 INCMP _ 0 INCMP enter_yob 1 INCMP enter_yob 2 diff --git a/services/registration/select_language.vis b/services/registration/select_language.vis index aa83e0c..54f08e9 100644 --- a/services/registration/select_language.vis +++ b/services/registration/select_language.vis @@ -1,6 +1,6 @@ MOUT english 0 MOUT kiswahili 1 HALT -INCMP set_default 0 +INCMP set_eng 0 INCMP set_swa 1 INCMP . * diff --git a/services/registration/set_default.vis b/services/registration/set_eng.vis similarity index 100% rename from services/registration/set_default.vis rename to services/registration/set_eng.vis diff --git a/services/registration/update_success_swa b/services/registration/update_success_swa index 834ba86..640a0bd 100644 --- a/services/registration/update_success_swa +++ b/services/registration/update_success_swa @@ -1 +1 @@ -Akaunti imeupdatiwa \ No newline at end of file +Ombi lako la kuweka wasifu limefanikiwa diff --git a/services/registration/view_menu_swa b/services/registration/view_menu_swa new file mode 100644 index 0000000..bd84b19 --- /dev/null +++ b/services/registration/view_menu_swa @@ -0,0 +1 @@ +Angalia Wasifu \ No newline at end of file