Compare commits
	
		
			5 Commits
		
	
	
		
			master
			...
			wip-pin-gu
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| edc6e9a3f9 | |||
| 27c7d4f35b | |||
| 65b86ddeef | |||
| 7eb0bf3369 | |||
| eefdc1c0f2 | 
| @ -1,14 +0,0 @@ | |||||||
| /** |  | ||||||
| !/cmd/africastalking |  | ||||||
| !/cmd/ssh |  | ||||||
| !/common |  | ||||||
| !/config |  | ||||||
| !/initializers |  | ||||||
| !/internal |  | ||||||
| !/models |  | ||||||
| !/remote |  | ||||||
| !/services |  | ||||||
| !/LICENSE |  | ||||||
| !/README.md |  | ||||||
| !/go.* |  | ||||||
| !/.env.example |  | ||||||
							
								
								
									
										20
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								.env.example
									
									
									
									
									
								
							| @ -1,20 +0,0 @@ | |||||||
| #Serve Http |  | ||||||
| PORT=7123 |  | ||||||
| HOST=127.0.0.1 |  | ||||||
| 
 |  | ||||||
| #AfricasTalking USSD POST endpoint |  | ||||||
| AT_ENDPOINT=/ussd/africastalking |  | ||||||
| 
 |  | ||||||
| #PostgreSQL |  | ||||||
| DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd |  | ||||||
| #DB_TIMEZONE=Africa/Nairobi |  | ||||||
| #DB_SCHEMA=vise |  | ||||||
| 
 |  | ||||||
| #External API Calls |  | ||||||
| CUSTODIAL_URL_BASE=http://localhost:5003 |  | ||||||
| BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr |  | ||||||
| DATA_URL_BASE=http://localhost:5006 |  | ||||||
| 
 |  | ||||||
| #Language |  | ||||||
| DEFAULT_LANGUAGE=eng |  | ||||||
| LANGUAGES=eng, swa |  | ||||||
							
								
								
									
										56
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										56
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
								
							| @ -1,56 +0,0 @@ | |||||||
| name: release |  | ||||||
| 
 |  | ||||||
| on: |  | ||||||
|   push: |  | ||||||
|     tags: |  | ||||||
|       - "v*" |  | ||||||
| 
 |  | ||||||
| jobs: |  | ||||||
|   docker: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - name: Set up QEMU |  | ||||||
|         uses: docker/setup-qemu-action@v2 |  | ||||||
| 
 |  | ||||||
|       - name: Set up Docker Buildx |  | ||||||
|         uses: docker/setup-buildx-action@v2 |  | ||||||
| 
 |  | ||||||
|       - name: Check out repo |  | ||||||
|         uses: actions/checkout@v3 |  | ||||||
|         with: |  | ||||||
|           fetch-depth: 0 |  | ||||||
| 
 |  | ||||||
|       - name: Cache Docker layers |  | ||||||
|         uses: actions/cache@v3 |  | ||||||
|         with: |  | ||||||
|           path: /tmp/.buildx-cache |  | ||||||
|           key: ${{ runner.os }}-buildx-${{ github.sha }} |  | ||||||
|           restore-keys: | |  | ||||||
|             ${{ runner.os }}-buildx- |  | ||||||
| 
 |  | ||||||
|       - name: Login to GHCR Docker registry |  | ||||||
|         uses: docker/login-action@v1 |  | ||||||
|         with: |  | ||||||
|           registry: ghcr.io |  | ||||||
|           username: ${{ github.repository_owner }} |  | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
| 
 |  | ||||||
|       - name: Set outputs |  | ||||||
|         run: | |  | ||||||
|           echo "RELEASE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV \ |  | ||||||
|           && echo "RELEASE_SHORT_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV |  | ||||||
| 
 |  | ||||||
|       - name: Build and push image |  | ||||||
|         uses: docker/build-push-action@v2 |  | ||||||
|         with: |  | ||||||
|           context: ./ |  | ||||||
|           file: ./Dockerfile |  | ||||||
|           platforms: linux/amd64 |  | ||||||
|           push: true |  | ||||||
|           build-args: | |  | ||||||
|             BUILD=${{ env.RELEASE_SHORT_COMMIT }} |  | ||||||
|           cache-from: type=local,src=/tmp/.buildx-cache |  | ||||||
|           cache-to: type=local,dest=/tmp/.buildx-cache |  | ||||||
|           tags: | |  | ||||||
|             ghcr.io/grassrootseconomics/urdt-ussd:latest |  | ||||||
|             ghcr.io/grassrootseconomics/urdt-ussd:${{ env.RELEASE_TAG }} |  | ||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -4,6 +4,3 @@ go.work* | |||||||
| **/*/*.bin | **/*/*.bin | ||||||
| **/*/.state/ | **/*/.state/ | ||||||
| cmd/.state/ | cmd/.state/ | ||||||
| id_* |  | ||||||
| *.gdbm |  | ||||||
| *.log |  | ||||||
|  | |||||||
							
								
								
									
										44
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,44 +0,0 @@ | |||||||
| FROM golang:1.23.0-bookworm AS build |  | ||||||
| 
 |  | ||||||
| ENV CGO_ENABLED=1 |  | ||||||
| 
 |  | ||||||
| ARG BUILDPLATFORM |  | ||||||
| ARG TARGETPLATFORM |  | ||||||
| ARG BUILD=dev |  | ||||||
| 
 |  | ||||||
| WORKDIR /build |  | ||||||
| COPY . . |  | ||||||
| RUN apt update && apt install libgdbm-dev |  | ||||||
| RUN git clone https://git.defalsify.org/vise.git go-vise |  | ||||||
| 
 |  | ||||||
| WORKDIR /build/services/registration |  | ||||||
| RUN echo "Compiling go-vise files" |  | ||||||
| RUN make VISE_PATH=/build/go-vise -B |  | ||||||
| 
 |  | ||||||
| WORKDIR /build |  | ||||||
| RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM" |  | ||||||
| RUN go mod download |  | ||||||
| RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go |  | ||||||
| RUN go build -tags logtrace -o ussd-ssh -ldflags="-X main.build=${BUILD} -s -w" cmd/ssh/main.go |  | ||||||
| 
 |  | ||||||
| FROM debian:bookworm-slim |  | ||||||
| 
 |  | ||||||
| ENV DEBIAN_FRONTEND=noninteractive |  | ||||||
| 
 |  | ||||||
| RUN apt update && apt install libgdbm-dev ca-certificates -y |  | ||||||
| RUN apt-get clean && rm -rf /var/lib/apt/lists/* |  | ||||||
| 
 |  | ||||||
| WORKDIR /service |  | ||||||
| 
 |  | ||||||
| COPY --from=build /build/ussd-africastalking . |  | ||||||
| COPY --from=build /build/ussd-ssh . |  | ||||||
| COPY --from=build /build/LICENSE . |  | ||||||
| COPY --from=build /build/README.md . |  | ||||||
| COPY --from=build /build/services ./services |  | ||||||
| COPY --from=build /build/.env.example . |  | ||||||
| RUN mv .env.example .env |  | ||||||
| 
 |  | ||||||
| EXPOSE 7123 |  | ||||||
| EXPOSE 7122 |  | ||||||
| 
 |  | ||||||
| CMD ["./ussd-africastalking"] |  | ||||||
							
								
								
									
										89
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								README.md
									
									
									
									
									
								
							| @ -1,91 +1,8 @@ | |||||||
| # URDT USSD service | # ussd | ||||||
| 
 | 
 | ||||||
| This is a USSD service built using the [go-vise](https://github.com/nolash/go-vise) engine. | > USSD | ||||||
| 
 | 
 | ||||||
| ## Prerequisites | USSD service. | ||||||
| ### 1. [go-vise](https://github.com/nolash/go-vise) |  | ||||||
| 
 |  | ||||||
| Set up `go-vise` by cloning the repository into a separate directory. The main upstream repository is hosted at: `https://git.defalsify.org/vise.git` |  | ||||||
| ``` |  | ||||||
| git clone https://git.defalsify.org/vise.git |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ## Setup |  | ||||||
| 1. Clone the ussd repo in its own directory |  | ||||||
| 
 |  | ||||||
|     ``` |  | ||||||
|     git clone https://git.grassecon.net/urdt/ussd.git |  | ||||||
|     ``` |  | ||||||
| 
 |  | ||||||
| 2. Navigate to the project directory. |  | ||||||
| 3. Enter the `services/registration` subfolder: |  | ||||||
|     ``` |  | ||||||
|     cd services/registration |  | ||||||
|     ``` |  | ||||||
| 4. make the .bin files from the .vis files |  | ||||||
|     ``` |  | ||||||
|     make VISE_PATH=/var/path/to/your/go-vise -B |  | ||||||
|     ``` |  | ||||||
| 5. Return to the project root (`cd ../..`) |  | ||||||
| 6. Run the USSD menu  |  | ||||||
|     ``` |  | ||||||
|     go run cmd/main.go -session-id=0712345678 |  | ||||||
|     ``` |  | ||||||
| ## Running the different binaries |  | ||||||
| 1. ### CLI:  |  | ||||||
|     ``` |  | ||||||
|     go run cmd/main.go -session-id=0712345678 |  | ||||||
|     ``` |  | ||||||
| 2. ### Africastalking:  |  | ||||||
|     ``` |  | ||||||
|     go run cmd/africastalking/main.go |  | ||||||
|     ``` |  | ||||||
| 3. ### Async:  |  | ||||||
|     ``` |  | ||||||
|     go run cmd/async/main.go |  | ||||||
|     ``` |  | ||||||
| 4. ### Http:  |  | ||||||
|     ``` |  | ||||||
|     go run cmd/http/main.go |  | ||||||
|     ``` |  | ||||||
|      |  | ||||||
| ## Flags |  | ||||||
| Below are the supported flags: |  | ||||||
| 
 |  | ||||||
| 1. `-session-id`:  |  | ||||||
|      |  | ||||||
|     Specifies the session ID. (CLI only).  |  | ||||||
|      |  | ||||||
|     Default: `075xx2123`. |  | ||||||
| 
 |  | ||||||
|     Example: |  | ||||||
|     ``` |  | ||||||
|     go run cmd/main.go -session-id=0712345678 |  | ||||||
|     ``` |  | ||||||
| 
 |  | ||||||
| 2. `-d`:  |  | ||||||
| 
 |  | ||||||
|     Enables engine debug output.  |  | ||||||
|      |  | ||||||
|     Default: `false`. |  | ||||||
| 
 |  | ||||||
|     Example: |  | ||||||
|     ``` |  | ||||||
|     go run cmd/main.go -session-id=0712345678 -d |  | ||||||
|     ``` |  | ||||||
| 
 |  | ||||||
| 3. `-db`:  |  | ||||||
| 
 |  | ||||||
|     Specifies the database type. |  | ||||||
|      |  | ||||||
|     Default: `gdbm`. |  | ||||||
| 
 |  | ||||||
|     Example: |  | ||||||
|     ``` |  | ||||||
|     go run cmd/main.go -session-id=0712345678 -d -db=postgres |  | ||||||
|     ``` |  | ||||||
| 
 |  | ||||||
|     >Note: If using `-db=postgres`, ensure PostgreSQL is running with the connection details specified in your `.env` file. |  | ||||||
| 
 | 
 | ||||||
| ## License | ## License | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -9,101 +9,207 @@ import ( | |||||||
| 	"os/signal" | 	"os/signal" | ||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 
 | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/asm" | ||||||
|  | 	"git.defalsify.org/vise.git/db" | ||||||
|  | 	fsdb "git.defalsify.org/vise.git/db/fs" | ||||||
|  | 	gdbmdb "git.defalsify.org/vise.git/db/gdbm" | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"git.defalsify.org/vise.git/engine" | ||||||
| 	"git.defalsify.org/vise.git/lang" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" | 	"git.defalsify.org/vise.git/logging" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/config" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/args" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/http/at" | 	"git.grassecon.net/urdt/ussd/internal/handlers/ussd" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	logg          = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id") | 	logg = logging.NewVanilla() | ||||||
| 	scriptDir     = path.Join("services", "registration") | 	scriptDir = path.Join("services", "registration") | ||||||
| 	build         = "dev" |  | ||||||
| 	menuSeparator = ": " |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | type atRequestParser struct {} | ||||||
| 	initializers.LoadEnvVariables() | 
 | ||||||
|  | func(arp *atRequestParser) GetSessionId(rq any) (string, error) { | ||||||
|  | 	rqv, ok := rq.(*http.Request) | ||||||
|  | 	if !ok { | ||||||
|  | 		return "", handlers.ErrInvalidRequest | ||||||
|  | 	} | ||||||
|  | 	if err := rqv.ParseForm(); err != nil { | ||||||
|  | 		return "", fmt.Errorf("failed to parse form data: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	phoneNumber := rqv.FormValue("phoneNumber") | ||||||
|  | 	if phoneNumber == "" { | ||||||
|  | 		return "", fmt.Errorf("no phone number found") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return phoneNumber, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func main() { | func(arp *atRequestParser) GetInput(rq any) ([]byte, error) { | ||||||
| 	config.LoadConfig() | 	rqv, ok := rq.(*http.Request) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, handlers.ErrInvalidRequest | ||||||
|  | 	} | ||||||
|  | 	if err := rqv.ParseForm(); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to parse form data: %v", err) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	var connStr string | 	text := rqv.FormValue("text") | ||||||
|  | 
 | ||||||
|  | 	parts := strings.Split(text, "*") | ||||||
|  | 	if len(parts) == 0 { | ||||||
|  | 		return nil, fmt.Errorf("no input found") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return []byte(parts[len(parts)-1]), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | func getFlags(fp string, debug bool) (*asm.FlagParser, error) { | ||||||
|  | 	flagParser := asm.NewFlagParser().WithDebug() | ||||||
|  | 	_, err := flagParser.Load(fp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return flagParser, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) { | ||||||
|  | 
 | ||||||
|  | 	ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	rs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) | ||||||
|  | 	rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) | ||||||
|  | 	rs.AddLocalFunc("save_pin", ussdHandlers.SavePin) | ||||||
|  | 	rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) | ||||||
|  | 	rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) | ||||||
|  | 	rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) | ||||||
|  | 	rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) | ||||||
|  | 	rs.AddLocalFunc("quit", ussdHandlers.Quit) | ||||||
|  | 	rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) | ||||||
|  | 	rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) | ||||||
|  | 	rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) | ||||||
|  | 	rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) | ||||||
|  | 	rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) | ||||||
|  | 	rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) | ||||||
|  | 	rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) | ||||||
|  | 	rs.AddLocalFunc("get_sender", ussdHandlers.GetSender) | ||||||
|  | 	rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) | ||||||
|  | 	rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) | ||||||
|  | 	rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) | ||||||
|  | 	rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) | ||||||
|  | 	rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) | ||||||
|  | 	rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) | ||||||
|  | 	rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) | ||||||
|  | 	rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) | ||||||
|  | 	rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance) | ||||||
|  | 	rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) | ||||||
|  | 	rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) | ||||||
|  | 	rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) | ||||||
|  | 	rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) | ||||||
|  | 	rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) | ||||||
|  | 	rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit) | ||||||
|  | 	rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) | ||||||
|  | 	rs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) | ||||||
|  | 	rs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) | ||||||
|  | 	rs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) | ||||||
|  | 	rs.AddLocalFunc("quit_with_help",ussdHandlers.QuitWithHelp) | ||||||
|  | 
 | ||||||
|  | 	return ussdHandlers, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ensureDbDir(dbDir string) error { | ||||||
|  | 	err := os.MkdirAll(dbDir, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("state dir create exited with error: %v\n", err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getStateStore(dbDir string, ctx context.Context) (db.Db, error) { | ||||||
|  | 	store := gdbmdb.NewGdbmDb() | ||||||
|  | 	storeFile := path.Join(dbDir, "state.gdbm") | ||||||
|  | 	store.Connect(ctx, storeFile) | ||||||
|  | 	return store, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getUserdataDb(dbDir string, ctx context.Context) db.Db { | ||||||
|  | 	store := gdbmdb.NewGdbmDb() | ||||||
|  | 	storeFile := path.Join(dbDir, "userdata.gdbm") | ||||||
|  | 	store.Connect(ctx, storeFile) | ||||||
|  | 	 | ||||||
|  | 	return store | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) { | ||||||
|  | 	store := fsdb.NewFsDb() | ||||||
|  | 	err := store.Connect(ctx, resourceDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	rfs := resource.NewDbResource(store) | ||||||
|  | 	return rfs, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	var dbDir string | ||||||
| 	var resourceDir string | 	var resourceDir string | ||||||
| 	var size uint | 	var size uint | ||||||
| 	var engineDebug bool | 	var engineDebug bool | ||||||
|  | 	var stateDebug bool | ||||||
| 	var host string | 	var host string | ||||||
| 	var port uint | 	var port uint | ||||||
| 	var err error | 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||||
| 	var gettextDir string |  | ||||||
| 	var langs args.LangVar |  | ||||||
| 
 |  | ||||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | 	flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | 	flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output") | ||||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | 	flag.UintVar(&size, "s", 160, "max size of output") | ||||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | 	flag.StringVar(&host, "h", "127.0.0.1", "http host") | ||||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | 	flag.UintVar(&port, "p", 7123, "http port") | ||||||
| 	flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") |  | ||||||
| 	flag.Var(&langs, "language", "add symbol resolution for language") |  | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	if connStr == "" { | 	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir,  "outputsize", size) | ||||||
| 		connStr = config.DbConn |  | ||||||
| 	} |  | ||||||
| 	connData, err := storage.ToConnData(connStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	logg.Infof("start command", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size) |  | ||||||
| 
 | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	ln, err := lang.LanguageFromCode(config.DefaultLanguage) | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
|  | 	flagParser, err := getFlags(pfp, true) | ||||||
|  | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, "default language set error: %v", err) |  | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 	ctx = context.WithValue(ctx, "Language", ln) |  | ||||||
| 
 |  | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") |  | ||||||
| 
 | 
 | ||||||
| 	cfg := engine.Config{ | 	cfg := engine.Config{ | ||||||
| 		Root:          "root", | 		Root:       "root", | ||||||
| 		OutputSize:    uint32(size), | 		OutputSize: uint32(size), | ||||||
| 		FlagCount:     uint32(128), | 		FlagCount:  uint32(16), | ||||||
| 		MenuSeparator: menuSeparator, | 	} | ||||||
|  | 	if stateDebug { | ||||||
|  | 		cfg.StateDebug = true | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	if engineDebug { | 	if engineDebug { | ||||||
| 		cfg.EngineDebug = true | 		cfg.EngineDebug = true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | 	rs, err := getResource(resourceDir, ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) | 	err = ensureDbDir(dbDir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	userdataStore, err := menuStorageService.GetUserdataDb(ctx) | 	userdataStore := getUserdataDb(dbDir, ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| @ -115,42 +221,25 @@ func main() { | |||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | 	hl, err := getHandler(flagParser, dbResource, userdataStore) | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	lhs.SetDataStore(&userdataStore) |  | ||||||
| 
 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	accountService := remote.AccountService{} | 	stateStore, err := getStateStore(dbDir, ctx) | ||||||
| 	hl, err := lhs.GetHandler(&accountService) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	stateStore, err := menuStorageService.GetStateStore(ctx) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 	defer stateStore.Close() | 	defer stateStore.Close() | ||||||
| 
 | 
 | ||||||
| 	rp := &at.ATRequestParser{} | 	rp := &atRequestParser{} | ||||||
| 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | ||||||
| 	sh := at.NewATSessionHandler(bsh) | 	sh := httpserver.NewATSessionHandler(bsh) | ||||||
| 
 |  | ||||||
| 	mux := http.NewServeMux() |  | ||||||
| 	mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) |  | ||||||
| 
 |  | ||||||
| 	s := &http.Server{ | 	s := &http.Server{ | ||||||
| 		Addr:    fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), | 		Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), | ||||||
| 		Handler: mux, | 		Handler: sh, | ||||||
| 	} | 	} | ||||||
| 	s.RegisterOnShutdown(sh.Shutdown) | 	s.RegisterOnShutdown(sh.Shutdown) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -9,113 +9,179 @@ import ( | |||||||
| 	"path" | 	"path" | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 
 | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/asm" | ||||||
|  | 	"git.defalsify.org/vise.git/db" | ||||||
|  | 	fsdb "git.defalsify.org/vise.git/db/fs" | ||||||
|  | 	gdbmdb "git.defalsify.org/vise.git/db/gdbm" | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"git.defalsify.org/vise.git/engine" | ||||||
| 	"git.defalsify.org/vise.git/lang" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/config" | 	"git.grassecon.net/urdt/ussd/internal/handlers/ussd" | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/args" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	logg          = logging.NewVanilla() | 	logg = logging.NewVanilla() | ||||||
| 	scriptDir     = path.Join("services", "registration") | 	scriptDir = path.Join("services", "registration") | ||||||
| 	menuSeparator = ": " |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { |  | ||||||
| 	initializers.LoadEnvVariables() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type asyncRequestParser struct { | type asyncRequestParser struct { | ||||||
| 	sessionId string | 	sessionId string | ||||||
| 	input     []byte | 	input []byte | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *asyncRequestParser) GetSessionId(ctx context.Context, r any) (string, error) { | func(p *asyncRequestParser) GetSessionId(r any) (string, error) { | ||||||
| 	return p.sessionId, nil | 	return p.sessionId, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *asyncRequestParser) GetInput(r any) ([]byte, error) { | func(p *asyncRequestParser) GetInput(r any) ([]byte, error) { | ||||||
| 	return p.input, nil | 	return p.input, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func main() { | func getFlags(fp string, debug bool) (*asm.FlagParser, error) { | ||||||
| 	config.LoadConfig() | 	flagParser := asm.NewFlagParser().WithDebug() | ||||||
|  | 	_, err := flagParser.Load(fp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return flagParser, nil | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 	var connStr string | func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) { | ||||||
|  | 
 | ||||||
|  | 	ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	rs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) | ||||||
|  | 	rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) | ||||||
|  | 	rs.AddLocalFunc("save_pin", ussdHandlers.SavePin) | ||||||
|  | 	rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) | ||||||
|  | 	rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) | ||||||
|  | 	rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) | ||||||
|  | 	rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) | ||||||
|  | 	rs.AddLocalFunc("quit", ussdHandlers.Quit) | ||||||
|  | 	rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) | ||||||
|  | 	rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) | ||||||
|  | 	rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) | ||||||
|  | 	rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) | ||||||
|  | 	rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) | ||||||
|  | 	rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) | ||||||
|  | 	rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) | ||||||
|  | 	rs.AddLocalFunc("get_sender", ussdHandlers.GetSender) | ||||||
|  | 	rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) | ||||||
|  | 	rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) | ||||||
|  | 	rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) | ||||||
|  | 	rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) | ||||||
|  | 	rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) | ||||||
|  | 	rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) | ||||||
|  | 	rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) | ||||||
|  | 	rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) | ||||||
|  | 	rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance) | ||||||
|  | 	rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) | ||||||
|  | 	rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) | ||||||
|  | 	rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) | ||||||
|  | 	rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) | ||||||
|  | 	rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) | ||||||
|  | 	rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit) | ||||||
|  | 	rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) | ||||||
|  | 	rs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) | ||||||
|  | 	rs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) | ||||||
|  | 	rs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) | ||||||
|  | 	rs.AddLocalFunc("quit_with_help",ussdHandlers.QuitWithHelp) | ||||||
|  | 
 | ||||||
|  | 	return ussdHandlers, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ensureDbDir(dbDir string) error { | ||||||
|  | 	err := os.MkdirAll(dbDir, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("state dir create exited with error: %v\n", err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getStateStore(dbDir string, ctx context.Context) (db.Db, error) { | ||||||
|  | 	store := gdbmdb.NewGdbmDb() | ||||||
|  | 	storeFile := path.Join(dbDir, "state.gdbm") | ||||||
|  | 	store.Connect(ctx, storeFile) | ||||||
|  | 	return store, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getUserdataDb(dbDir string, ctx context.Context) db.Db { | ||||||
|  | 	store := gdbmdb.NewGdbmDb() | ||||||
|  | 	storeFile := path.Join(dbDir, "userdata.gdbm") | ||||||
|  | 	store.Connect(ctx, storeFile) | ||||||
|  | 	 | ||||||
|  | 	return store | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) { | ||||||
|  | 	store := fsdb.NewFsDb() | ||||||
|  | 	err := store.Connect(ctx, resourceDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	rfs := resource.NewDbResource(store) | ||||||
|  | 	return rfs, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
| 	var sessionId string | 	var sessionId string | ||||||
|  | 	var dbDir string | ||||||
| 	var resourceDir string | 	var resourceDir string | ||||||
| 	var size uint | 	var size uint | ||||||
| 	var engineDebug bool | 	var engineDebug bool | ||||||
|  | 	var stateDebug bool | ||||||
| 	var host string | 	var host string | ||||||
| 	var port uint | 	var port uint | ||||||
| 	var err error |  | ||||||
| 	var gettextDir string |  | ||||||
| 	var langs args.LangVar |  | ||||||
| 
 |  | ||||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||||
|  | 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | 	flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | 	flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output") | ||||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | 	flag.UintVar(&size, "s", 160, "max size of output") | ||||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | 	flag.StringVar(&host, "h", "127.0.0.1", "http host") | ||||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | 	flag.UintVar(&port, "p", 7123, "http port") | ||||||
| 	flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") |  | ||||||
| 	flag.Var(&langs, "language", "add symbol resolution for language") |  | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	if connStr == "" { | 	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir,  "outputsize", size, "sessionId", sessionId) | ||||||
| 		connStr = config.DbConn |  | ||||||
| 	} |  | ||||||
| 	connData, err := storage.ToConnData(connStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) |  | ||||||
| 
 | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
|  | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
|  | 	flagParser, err := getFlags(pfp, true) | ||||||
| 
 | 
 | ||||||
| 	ln, err := lang.LanguageFromCode(config.DefaultLanguage) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, "default language set error: %v", err) |  | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 	ctx = context.WithValue(ctx, "Language", ln) |  | ||||||
| 
 |  | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") |  | ||||||
| 
 | 
 | ||||||
| 	cfg := engine.Config{ | 	cfg := engine.Config{ | ||||||
| 		Root:          "root", | 		Root:       "root", | ||||||
| 		OutputSize:    uint32(size), | 		OutputSize: uint32(size), | ||||||
| 		FlagCount:     uint32(128), | 		FlagCount:  uint32(16), | ||||||
| 		MenuSeparator: menuSeparator, | 	} | ||||||
|  | 	if stateDebug { | ||||||
|  | 		cfg.StateDebug = true | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	if engineDebug { | 	if engineDebug { | ||||||
| 		cfg.EngineDebug = true | 		cfg.EngineDebug = true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | 	rs, err := getResource(resourceDir, ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) | 	err = ensureDbDir(dbDir) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	userdataStore, err := menuStorageService.GetUserdataDb(ctx) | 	userdataStore := getUserdataDb(dbDir, ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| @ -127,17 +193,13 @@ func main() { | |||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | 	hl, err := getHandler(flagParser, dbResource, userdataStore) | ||||||
| 	lhs.SetDataStore(&userdataStore) |  | ||||||
| 	accountService := remote.AccountService{} |  | ||||||
| 
 |  | ||||||
| 	hl, err := lhs.GetHandler(&accountService) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	stateStore, err := menuStorageService.GetStateStore(ctx) | 	stateStore, err := getStateStore(dbDir, ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| @ -150,7 +212,7 @@ func main() { | |||||||
| 	sh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | 	sh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | ||||||
| 	cfg.SessionId = sessionId | 	cfg.SessionId = sessionId | ||||||
| 	rqs := handlers.RequestSession{ | 	rqs := handlers.RequestSession{ | ||||||
| 		Ctx:    ctx, | 		Ctx: ctx, | ||||||
| 		Writer: os.Stdout, | 		Writer: os.Stdout, | ||||||
| 		Config: cfg, | 		Config: cfg, | ||||||
| 	} | 	} | ||||||
| @ -170,27 +232,23 @@ func main() { | |||||||
| 	for true { | 	for true { | ||||||
| 		rqs, err = sh.Process(rqs) | 		rqs, err = sh.Process(rqs) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logg.ErrorCtxf(ctx, "error in process: %v", "err", err) |  | ||||||
| 			fmt.Errorf("error in process: %v", err) | 			fmt.Errorf("error in process: %v", err) | ||||||
| 			os.Exit(1) | 			os.Exit(1) | ||||||
| 		} | 		} | ||||||
| 		rqs, err = sh.Output(rqs) | 		rqs, err = sh.Output(rqs) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logg.ErrorCtxf(ctx, "error in output: %v", "err", err) |  | ||||||
| 			fmt.Errorf("error in output: %v", err) | 			fmt.Errorf("error in output: %v", err) | ||||||
| 			os.Exit(1) | 			os.Exit(1) | ||||||
| 		} | 		} | ||||||
| 		rqs, err = sh.Reset(rqs) | 		rqs, err = sh.Reset(rqs) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logg.ErrorCtxf(ctx, "error in reset: %v", "err", err) |  | ||||||
| 			fmt.Errorf("error in reset: %v", err) | 			fmt.Errorf("error in reset: %v", err) | ||||||
| 			os.Exit(1) | 			os.Exit(1) | ||||||
| 		} | 		} | ||||||
| 		fmt.Println("") | 		fmt.Println("") | ||||||
| 		_, err = fmt.Scanln(&rqs.Input) | 		_, err = fmt.Scanln(&rqs.Input) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logg.ErrorCtxf(ctx, "error in input", "err", err) | 		fmt.Errorf("error in input: %v", err) | ||||||
| 			fmt.Errorf("error in input: %v", err) |  | ||||||
| 			os.Exit(1) | 			os.Exit(1) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
							
								
								
									
										183
									
								
								cmd/http/main.go
									
									
									
									
									
								
							
							
						
						
									
										183
									
								
								cmd/http/main.go
									
									
									
									
									
								
							| @ -11,95 +11,165 @@ import ( | |||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 
 | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/asm" | ||||||
|  | 	"git.defalsify.org/vise.git/db" | ||||||
|  | 	fsdb "git.defalsify.org/vise.git/db/fs" | ||||||
|  | 	gdbmdb "git.defalsify.org/vise.git/db/gdbm" | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"git.defalsify.org/vise.git/engine" | ||||||
| 	"git.defalsify.org/vise.git/lang" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/config" | 	"git.grassecon.net/urdt/ussd/internal/handlers/ussd" | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/args" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	logg          = logging.NewVanilla() | 	logg = logging.NewVanilla() | ||||||
| 	scriptDir     = path.Join("services", "registration") | 	scriptDir = path.Join("services", "registration") | ||||||
| 	menuSeparator = ": " |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func getFlags(fp string, debug bool) (*asm.FlagParser, error) { | ||||||
| 	initializers.LoadEnvVariables() | 	flagParser := asm.NewFlagParser().WithDebug() | ||||||
|  | 	_, err := flagParser.Load(fp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return flagParser, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func main() { | func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) { | ||||||
| 	config.LoadConfig() |  | ||||||
| 
 | 
 | ||||||
| 	var connStr string | 	ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	rs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) | ||||||
|  | 	rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) | ||||||
|  | 	rs.AddLocalFunc("save_pin", ussdHandlers.SavePin) | ||||||
|  | 	rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) | ||||||
|  | 	rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) | ||||||
|  | 	rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) | ||||||
|  | 	rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) | ||||||
|  | 	rs.AddLocalFunc("quit", ussdHandlers.Quit) | ||||||
|  | 	rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) | ||||||
|  | 	rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) | ||||||
|  | 	rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) | ||||||
|  | 	rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) | ||||||
|  | 	rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) | ||||||
|  | 	rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) | ||||||
|  | 	rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) | ||||||
|  | 	rs.AddLocalFunc("get_sender", ussdHandlers.GetSender) | ||||||
|  | 	rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) | ||||||
|  | 	rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) | ||||||
|  | 	rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) | ||||||
|  | 	rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) | ||||||
|  | 	rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) | ||||||
|  | 	rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) | ||||||
|  | 	rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) | ||||||
|  | 	rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) | ||||||
|  | 	rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance) | ||||||
|  | 	rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) | ||||||
|  | 	rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) | ||||||
|  | 	rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) | ||||||
|  | 	rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) | ||||||
|  | 	rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) | ||||||
|  | 	rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit) | ||||||
|  | 	rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) | ||||||
|  | 	rs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) | ||||||
|  | 	rs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) | ||||||
|  | 	rs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) | ||||||
|  | 	rs.AddLocalFunc("quit_with_help",ussdHandlers.QuitWithHelp) | ||||||
|  | 
 | ||||||
|  | 	return ussdHandlers, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ensureDbDir(dbDir string) error { | ||||||
|  | 	err := os.MkdirAll(dbDir, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("state dir create exited with error: %v\n", err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getStateStore(dbDir string, ctx context.Context) (db.Db, error) { | ||||||
|  | 	store := gdbmdb.NewGdbmDb() | ||||||
|  | 	storeFile := path.Join(dbDir, "state.gdbm") | ||||||
|  | 	store.Connect(ctx, storeFile) | ||||||
|  | 	return store, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getUserdataDb(dbDir string, ctx context.Context) db.Db { | ||||||
|  | 	store := gdbmdb.NewGdbmDb() | ||||||
|  | 	storeFile := path.Join(dbDir, "userdata.gdbm") | ||||||
|  | 	store.Connect(ctx, storeFile) | ||||||
|  | 	 | ||||||
|  | 	return store | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) { | ||||||
|  | 	store := fsdb.NewFsDb() | ||||||
|  | 	err := store.Connect(ctx, resourceDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	rfs := resource.NewDbResource(store) | ||||||
|  | 	return rfs, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	var dbDir string | ||||||
| 	var resourceDir string | 	var resourceDir string | ||||||
| 	var size uint | 	var size uint | ||||||
| 	var engineDebug bool | 	var engineDebug bool | ||||||
|  | 	var stateDebug bool | ||||||
| 	var host string | 	var host string | ||||||
| 	var port uint | 	var port uint | ||||||
| 	var err error | 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||||
| 	var gettextDir string |  | ||||||
| 	var langs args.LangVar |  | ||||||
| 
 |  | ||||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | 	flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | 	flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output") | ||||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | 	flag.UintVar(&size, "s", 160, "max size of output") | ||||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | 	flag.StringVar(&host, "h", "127.0.0.1", "http host") | ||||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | 	flag.UintVar(&port, "p", 7123, "http port") | ||||||
| 	flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") |  | ||||||
| 	flag.Var(&langs, "language", "add symbol resolution for language") |  | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	if connStr == "" { | 	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir,  "outputsize", size) | ||||||
| 		connStr = config.DbConn |  | ||||||
| 	} |  | ||||||
| 	connData, err := storage.ToConnData(connStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size) |  | ||||||
| 
 | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
|  | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
|  | 	flagParser, err := getFlags(pfp, true) | ||||||
| 
 | 
 | ||||||
| 	ln, err := lang.LanguageFromCode(config.DefaultLanguage) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, "default language set error: %v", err) |  | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 	ctx = context.WithValue(ctx, "Language", ln) |  | ||||||
| 
 |  | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") |  | ||||||
| 
 | 
 | ||||||
| 	cfg := engine.Config{ | 	cfg := engine.Config{ | ||||||
| 		Root:          "root", | 		Root:       "root", | ||||||
| 		OutputSize:    uint32(size), | 		OutputSize: uint32(size), | ||||||
| 		FlagCount:     uint32(128), | 		FlagCount:  uint32(16), | ||||||
| 		MenuSeparator: menuSeparator, | 	} | ||||||
|  | 	if stateDebug { | ||||||
|  | 		cfg.StateDebug = true | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	if engineDebug { | 	if engineDebug { | ||||||
| 		cfg.EngineDebug = true | 		cfg.EngineDebug = true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | 	rs, err := getResource(resourceDir, ctx) | ||||||
| 
 |  | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	userdataStore, err := menuStorageService.GetUserdataDb(ctx) | 	err = ensureDbDir(dbDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	userdataStore := getUserdataDb(dbDir, ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| @ -111,22 +181,13 @@ func main() { | |||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | 	hl, err := getHandler(flagParser, dbResource, userdataStore) | ||||||
| 	lhs.SetDataStore(&userdataStore) |  | ||||||
| 
 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	accountService := remote.AccountService{} | 	stateStore, err := getStateStore(dbDir, ctx) | ||||||
| 	hl, err := lhs.GetHandler(&accountService) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	stateStore, err := menuStorageService.GetStateStore(ctx) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| @ -137,7 +198,7 @@ func main() { | |||||||
| 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | ||||||
| 	sh := httpserver.ToSessionHandler(bsh) | 	sh := httpserver.ToSessionHandler(bsh) | ||||||
| 	s := &http.Server{ | 	s := &http.Server{ | ||||||
| 		Addr:    fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), | 		Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), | ||||||
| 		Handler: sh, | 		Handler: sh, | ||||||
| 	} | 	} | ||||||
| 	s.RegisterOnShutdown(sh.Shutdown) | 	s.RegisterOnShutdown(sh.Shutdown) | ||||||
|  | |||||||
							
								
								
									
										204
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										204
									
								
								cmd/main.go
									
									
									
									
									
								
							| @ -7,104 +7,164 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 
 | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/asm" | ||||||
|  | 	"git.defalsify.org/vise.git/db" | ||||||
|  | 	fsdb "git.defalsify.org/vise.git/db/fs" | ||||||
|  | 	gdbmdb "git.defalsify.org/vise.git/db/gdbm" | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"git.defalsify.org/vise.git/engine" | ||||||
| 	"git.defalsify.org/vise.git/lang" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" | 	"git.defalsify.org/vise.git/logging" | ||||||
|  | 	"git.defalsify.org/vise.git/persist" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 	"git.grassecon.net/urdt/ussd/config" | 	"git.grassecon.net/urdt/ussd/internal/handlers/ussd" | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/args" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	logg          = logging.NewVanilla() | 	logg      = logging.NewVanilla() | ||||||
| 	scriptDir     = path.Join("services", "registration") | 	scriptDir = path.Join("services", "registration") | ||||||
| 	menuSeparator = ": " |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func getParser(fp string, debug bool) (*asm.FlagParser, error) { | ||||||
| 	initializers.LoadEnvVariables() | 	flagParser := asm.NewFlagParser().WithDebug() | ||||||
|  | 	_, err := flagParser.Load(fp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return flagParser, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility
 | func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, pe *persist.Persister, userdataStore db.Db) (*ussd.Handlers, error) { | ||||||
| func main() { |  | ||||||
| 	config.LoadConfig() |  | ||||||
| 
 | 
 | ||||||
| 	var connStr string | 	ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	ussdHandlers = ussdHandlers.WithPersister(pe) | ||||||
|  | 	rs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) | ||||||
|  | 	rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) | ||||||
|  | 	rs.AddLocalFunc("save_pin", ussdHandlers.SavePin) | ||||||
|  | 	rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) | ||||||
|  | 	rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) | ||||||
|  | 	rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) | ||||||
|  | 	rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) | ||||||
|  | 	rs.AddLocalFunc("quit", ussdHandlers.Quit) | ||||||
|  | 	rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) | ||||||
|  | 	rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) | ||||||
|  | 	rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) | ||||||
|  | 	rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) | ||||||
|  | 	rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) | ||||||
|  | 	rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) | ||||||
|  | 	rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) | ||||||
|  | 	rs.AddLocalFunc("get_sender", ussdHandlers.GetSender) | ||||||
|  | 	rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) | ||||||
|  | 	rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) | ||||||
|  | 	rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) | ||||||
|  | 	rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) | ||||||
|  | 	rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) | ||||||
|  | 	rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) | ||||||
|  | 	rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) | ||||||
|  | 	rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) | ||||||
|  | 	rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance) | ||||||
|  | 	rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) | ||||||
|  | 	rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) | ||||||
|  | 	rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) | ||||||
|  | 	rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) | ||||||
|  | 	rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) | ||||||
|  | 	rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit) | ||||||
|  | 	rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) | ||||||
|  | 	rs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) | ||||||
|  | 	rs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) | ||||||
|  | 	rs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) | ||||||
|  | 	rs.AddLocalFunc("quit_with_help",ussdHandlers.QuitWithHelp) | ||||||
|  | 
 | ||||||
|  | 	return ussdHandlers, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ensureDbDir(dbDir string) error { | ||||||
|  | 	err := os.MkdirAll(dbDir, 0700) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("state dir create exited with error: %v\n", err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getPersister(dbDir string, ctx context.Context) (*persist.Persister, error) { | ||||||
|  | 	err := ensureDbDir(dbDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	store := gdbmdb.NewGdbmDb() | ||||||
|  | 	storeFile := path.Join(dbDir, "state.gdbm") | ||||||
|  | 	store.Connect(ctx, storeFile) | ||||||
|  | 	pr := persist.NewPersister(store) | ||||||
|  | 	return pr, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getUserdataDb(dbDir string, ctx context.Context) db.Db { | ||||||
|  | 	store := gdbmdb.NewGdbmDb() | ||||||
|  | 	storeFile := path.Join(dbDir, "userdata.gdbm") | ||||||
|  | 	store.Connect(ctx, storeFile) | ||||||
|  | 
 | ||||||
|  | 	return store | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) { | ||||||
|  | 	store := fsdb.NewFsDb() | ||||||
|  | 	err := store.Connect(ctx, resourceDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	rfs := resource.NewDbResource(store) | ||||||
|  | 	return rfs, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) *engine.DefaultEngine { | ||||||
|  | 	en := engine.NewEngine(cfg, rs) | ||||||
|  | 	en = en.WithPersister(pr) | ||||||
|  | 	return en | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	var dbDir string | ||||||
| 	var size uint | 	var size uint | ||||||
| 	var sessionId string | 	var sessionId string | ||||||
| 	var engineDebug bool | 	var debug bool | ||||||
| 	var resourceDir string |  | ||||||
| 	var err error |  | ||||||
| 	var gettextDir string |  | ||||||
| 	var langs args.LangVar |  | ||||||
| 
 |  | ||||||
| 	flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir") |  | ||||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||||
| 	flag.StringVar(&connStr, "c", "", "connection string") | 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | 	flag.BoolVar(&debug, "d", false, "use engine debug output") | ||||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | 	flag.UintVar(&size, "s", 160, "max size of output") | ||||||
| 	flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") |  | ||||||
| 	flag.Var(&langs, "language", "add symbol resolution for language") |  | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	if connStr == "" { | 	logg.Infof("start command", "dbdir", dbDir, "outputsize", size) | ||||||
| 		connStr = config.DbConn |  | ||||||
| 	} |  | ||||||
| 	connData, err := storage.ToConnData(connStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	logg.Infof("start command", "conn", connData, "outputsize", size) |  | ||||||
| 
 |  | ||||||
| 	if len(langs.Langs()) == 0 { |  | ||||||
| 		langs.Set(config.DefaultLanguage) |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
|  | 	flagParser, err := getParser(pfp, true) | ||||||
| 
 | 
 | ||||||
| 	ln, err := lang.LanguageFromCode(config.DefaultLanguage) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, "default language set error: %v", err) |  | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 	ctx = context.WithValue(ctx, "Language", ln) |  | ||||||
| 
 |  | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") |  | ||||||
| 
 | 
 | ||||||
| 	cfg := engine.Config{ | 	cfg := engine.Config{ | ||||||
| 		Root:          "root", | 		Root:       "root", | ||||||
| 		SessionId:     sessionId, | 		SessionId:  sessionId, | ||||||
| 		OutputSize:    uint32(size), | 		OutputSize: uint32(size), | ||||||
| 		FlagCount:     uint32(128), | 		FlagCount:  uint32(16), | ||||||
| 		MenuSeparator: menuSeparator, |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | 	rs, err := getResource(scriptDir, ctx) | ||||||
| 
 |  | ||||||
| 	if gettextDir != "" { |  | ||||||
| 		menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs()) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	pe, err := menuStorageService.GetPersister(ctx) | 	pe, err := getPersister(dbDir, ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	userdatastore, err := menuStorageService.GetUserdataDb(ctx) | 	store := getUserdataDb(dbDir, ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| @ -116,29 +176,25 @@ func main() { | |||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | 	hl, err := getHandler(flagParser, dbResource, pe, store) | ||||||
| 	lhs.SetDataStore(&userdatastore) |  | ||||||
| 	lhs.SetPersister(pe) |  | ||||||
| 
 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	accountService := remote.AccountService{} | 	en := getEngine(cfg, rs, pe) | ||||||
| 	hl, err := lhs.GetHandler(&accountService) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	en := lhs.GetEngine() |  | ||||||
| 	en = en.WithFirst(hl.Init) | 	en = en.WithFirst(hl.Init) | ||||||
| 	if engineDebug { | 	if debug { | ||||||
| 		en = en.WithDebug(nil) | 		en = en.WithDebug(nil) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = engine.Loop(ctx, en, os.Stdin, os.Stdout, nil) | 	_, err = en.Init(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = engine.Loop(ctx, en, os.Stdin, os.Stdout) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) | 		fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
|  | |||||||
| @ -1,34 +0,0 @@ | |||||||
| # URDT-USSD SSH server |  | ||||||
| 
 |  | ||||||
| An SSH server entry point for the vise engine. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Adding public keys for access |  | ||||||
| 
 |  | ||||||
| Map your (client) public key to a session identifier (e.g. phone number) |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i <session_id> [--dbdir <dbpath>] <client_publickey_filepath> |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Create a private key for the server |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| ssh-keygen -N "" -f <server_privatekey_filepath> |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Run the server |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| go run -v -tags logtrace ./cmd/ssh/main.go -h <host> -p <port> [--dbdir <dbpath>] <server_privatekey_filepath> |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## Connect to the server |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| ssh [-v] -T -p <port> -i <client_publickey_filepath> <host> |  | ||||||
| ``` |  | ||||||
							
								
								
									
										144
									
								
								cmd/ssh/main.go
									
									
									
									
									
								
							
							
						
						
									
										144
									
								
								cmd/ssh/main.go
									
									
									
									
									
								
							| @ -1,144 +0,0 @@ | |||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"flag" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"os/signal" |  | ||||||
| 	"path" |  | ||||||
| 	"sync" |  | ||||||
| 	"syscall" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| 	"git.defalsify.org/vise.git/engine" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/config" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/ssh" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	wg        sync.WaitGroup |  | ||||||
| 	keyStore  db.Db |  | ||||||
| 	logg      = logging.NewVanilla() |  | ||||||
| 	scriptDir = path.Join("services", "registration") |  | ||||||
| 
 |  | ||||||
| 	build = "dev" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	initializers.LoadEnvVariables() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func main() { |  | ||||||
| 	config.LoadConfig() |  | ||||||
| 
 |  | ||||||
| 	var connStr string |  | ||||||
| 	var authConnStr string |  | ||||||
| 	var resourceDir string |  | ||||||
| 	var size uint |  | ||||||
| 	var engineDebug bool |  | ||||||
| 	var stateDebug bool |  | ||||||
| 	var host string |  | ||||||
| 	var port uint |  | ||||||
| 	flag.StringVar(&connStr, "c", "", "connection string") |  | ||||||
| 	flag.StringVar(&authConnStr, "authdb", "", "auth connection string") |  | ||||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") |  | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") |  | ||||||
| 	flag.UintVar(&size, "s", 160, "max size of output") |  | ||||||
| 	flag.StringVar(&host, "h", "127.0.0.1", "socket host") |  | ||||||
| 	flag.UintVar(&port, "p", 7122, "socket port") |  | ||||||
| 	flag.Parse() |  | ||||||
| 
 |  | ||||||
| 	if connStr == "" { |  | ||||||
| 		connStr = config.DbConn |  | ||||||
| 	} |  | ||||||
| 	if authConnStr == "" { |  | ||||||
| 		authConnStr = connStr |  | ||||||
| 	} |  | ||||||
| 	connData, err := storage.ToConnData(connStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	authConnData, err := storage.ToConnData(authConnStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "auth connstr err: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	sshKeyFile := flag.Arg(0) |  | ||||||
| 	_, err = os.Stat(sshKeyFile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING") |  | ||||||
| 	logg.WarnCtxf(ctx, "!!!!! =======================") |  | ||||||
| 	logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!") |  | ||||||
| 	logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!") |  | ||||||
| 	logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)") |  | ||||||
| 
 |  | ||||||
| 	logg.Infof("start command", "conn", connData, "authconn", authConnData, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port) |  | ||||||
| 
 |  | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") |  | ||||||
| 
 |  | ||||||
| 	cfg := engine.Config{ |  | ||||||
| 		Root:       "root", |  | ||||||
| 		OutputSize: uint32(size), |  | ||||||
| 		FlagCount:  uint32(16), |  | ||||||
| 	} |  | ||||||
| 	if stateDebug { |  | ||||||
| 		cfg.StateDebug = true |  | ||||||
| 	} |  | ||||||
| 	if engineDebug { |  | ||||||
| 		cfg.EngineDebug = true |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	authKeyStore, err := ssh.NewSshKeyStore(ctx, authConnData.String()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "keystore file open error: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	defer func() { |  | ||||||
| 		logg.TraceCtxf(ctx, "shutdown auth key store reached") |  | ||||||
| 		err = authKeyStore.Close() |  | ||||||
| 		if err != nil { |  | ||||||
| 			logg.ErrorCtxf(ctx, "keystore close error", "err", err) |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| 
 |  | ||||||
| 	cint := make(chan os.Signal) |  | ||||||
| 	cterm := make(chan os.Signal) |  | ||||||
| 	signal.Notify(cint, os.Interrupt, syscall.SIGINT) |  | ||||||
| 	signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) |  | ||||||
| 
 |  | ||||||
| 	runner := &ssh.SshRunner{ |  | ||||||
| 		Cfg: cfg, |  | ||||||
| 		Debug: engineDebug, |  | ||||||
| 		FlagFile: pfp, |  | ||||||
| 		Conn: connData, |  | ||||||
| 		ResourceDir: resourceDir, |  | ||||||
| 		SrvKeyFile:  sshKeyFile, |  | ||||||
| 		Host:        host, |  | ||||||
| 		Port:        port, |  | ||||||
| 	} |  | ||||||
| 	go func() { |  | ||||||
| 		select { |  | ||||||
| 		case _ = <-cint: |  | ||||||
| 		case _ = <-cterm: |  | ||||||
| 		} |  | ||||||
| 		logg.TraceCtxf(ctx, "shutdown runner reached") |  | ||||||
| 		err := runner.Stop() |  | ||||||
| 		if err != nil { |  | ||||||
| 			logg.ErrorCtxf(ctx, "runner stop error", "err", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 	}() |  | ||||||
| 	runner.Run(ctx, authKeyStore) |  | ||||||
| } |  | ||||||
| @ -1,44 +0,0 @@ | |||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"flag" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/ssh" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func main() { |  | ||||||
| 	var dbDir string |  | ||||||
| 	var sessionId string |  | ||||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") |  | ||||||
| 	flag.StringVar(&sessionId, "i", "", "session id") |  | ||||||
| 	flag.Parse() |  | ||||||
| 
 |  | ||||||
| 	if sessionId == "" { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "empty session id\n") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 
 |  | ||||||
| 	sshKeyFile := flag.Arg(0) |  | ||||||
| 	if sshKeyFile == "" { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "missing key file argument\n") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	store, err := ssh.NewSshKeyStore(ctx, dbDir) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "%v\n", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	defer store.Close() |  | ||||||
| 
 |  | ||||||
| 	err = store.AddFromFile(ctx, sshKeyFile, sessionId) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "%v\n", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										134
									
								
								common/db.go
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								common/db.go
									
									
									
									
									
								
							| @ -1,134 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"encoding/binary" |  | ||||||
| 	"errors" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA.
 |  | ||||||
| //
 |  | ||||||
| // All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id.
 |  | ||||||
| //
 |  | ||||||
| // * The first byte is vise/db.DATATYPE_USERDATA
 |  | ||||||
| // * The last 2 bytes are the DataTyp value, big-endian.
 |  | ||||||
| // * The intermediate bytes are the id of the user context.
 |  | ||||||
| //
 |  | ||||||
| // All values are strings
 |  | ||||||
| type DataTyp uint16 |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	// API Tracking id to follow status of account creation
 |  | ||||||
| 	DATA_TRACKING_ID = iota |  | ||||||
| 	// EVM address returned from API on account creation
 |  | ||||||
| 	DATA_PUBLIC_KEY |  | ||||||
| 	// Currently active PIN used to authenticate ussd state change requests
 |  | ||||||
| 	DATA_ACCOUNT_PIN |  | ||||||
| 	// The first name of the user
 |  | ||||||
| 	DATA_FIRST_NAME |  | ||||||
| 	// The last name of the user
 |  | ||||||
| 	DATA_FAMILY_NAME |  | ||||||
| 	// The year-of-birth of the user
 |  | ||||||
| 	DATA_YOB |  | ||||||
| 	// The location of the user
 |  | ||||||
| 	DATA_LOCATION |  | ||||||
| 	// The gender of the user
 |  | ||||||
| 	DATA_GENDER |  | ||||||
| 	// The offerings description of the user
 |  | ||||||
| 	DATA_OFFERINGS |  | ||||||
| 	// The ethereum address of the recipient of an ongoing send request
 |  | ||||||
| 	DATA_RECIPIENT |  | ||||||
| 	// The voucher value amount of an ongoing send request
 |  | ||||||
| 	DATA_AMOUNT |  | ||||||
| 	// A general swap field for temporary values
 |  | ||||||
| 	DATA_TEMPORARY_VALUE |  | ||||||
| 	// Currently active voucher symbol of user
 |  | ||||||
| 	DATA_ACTIVE_SYM |  | ||||||
| 	// Voucher balance of user's currently active voucher
 |  | ||||||
| 	DATA_ACTIVE_BAL |  | ||||||
| 	// String boolean indicating whether use of PIN is blocked
 |  | ||||||
| 	DATA_BLOCKED_NUMBER |  | ||||||
| 	// Reverse mapping of a user's evm address to a session id.
 |  | ||||||
| 	DATA_PUBLIC_KEY_REVERSE |  | ||||||
| 	// Decimal count of the currently active voucher
 |  | ||||||
| 	DATA_ACTIVE_DECIMAL |  | ||||||
| 	// EVM address of the currently active voucher
 |  | ||||||
| 	DATA_ACTIVE_ADDRESS |  | ||||||
| 	//Holds count of the number of incorrect PIN attempts
 |  | ||||||
| 	DATA_INCORRECT_PIN_ATTEMPTS |  | ||||||
| 	//ISO 639 code for the selected language.
 |  | ||||||
| 	DATA_SELECTED_LANGUAGE_CODE |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	// List of valid voucher symbols in the user context.
 |  | ||||||
| 	DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota |  | ||||||
| 	// List of voucher balances for vouchers valid in the user context.
 |  | ||||||
| 	DATA_VOUCHER_BALANCES |  | ||||||
| 	// List of voucher decimal counts for vouchers valid in the user context.
 |  | ||||||
| 	DATA_VOUCHER_DECIMALS |  | ||||||
| 	// List of voucher EVM addresses for vouchers valid in the user context.
 |  | ||||||
| 	DATA_VOUCHER_ADDRESSES |  | ||||||
| 	// List of senders for valid transactions in the user context.
 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	DATA_TX_SENDERS = 512 + iota |  | ||||||
| 	// List of recipients for valid transactions in the user context.
 |  | ||||||
| 	DATA_TX_RECIPIENTS |  | ||||||
| 	// List of voucher values for valid transactions in the user context.
 |  | ||||||
| 	DATA_TX_VALUES |  | ||||||
| 	// List of voucher EVM addresses for valid transactions in the user context.
 |  | ||||||
| 	DATA_TX_ADDRESSES |  | ||||||
| 	// List of valid transaction hashes in the user context.
 |  | ||||||
| 	DATA_TX_HASHES |  | ||||||
| 	// List of transaction dates for valid transactions in the user context.
 |  | ||||||
| 	DATA_TX_DATES |  | ||||||
| 	// List of voucher symbols for valid transactions in the user context.
 |  | ||||||
| 	DATA_TX_SYMBOLS |  | ||||||
| 	// List of voucher decimal counts for valid transactions in the user context.
 |  | ||||||
| 	DATA_TX_DECIMALS |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg = logging.NewVanilla().WithDomain("urdt-common") |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func typToBytes(typ DataTyp) []byte { |  | ||||||
| 	var b [2]byte |  | ||||||
| 	binary.BigEndian.PutUint16(b[:], uint16(typ)) |  | ||||||
| 	return b[:] |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func PackKey(typ DataTyp, data []byte) []byte { |  | ||||||
| 	v := typToBytes(typ) |  | ||||||
| 	return append(v, data...) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func StringToDataTyp(str string) (DataTyp, error) { |  | ||||||
| 	switch str { |  | ||||||
| 	case "DATA_FIRST_NAME": |  | ||||||
| 		return DATA_FIRST_NAME, nil |  | ||||||
| 	case "DATA_FAMILY_NAME": |  | ||||||
| 		return DATA_FAMILY_NAME, nil |  | ||||||
| 	case "DATA_YOB": |  | ||||||
| 		return DATA_YOB, nil |  | ||||||
| 	case "DATA_LOCATION": |  | ||||||
| 		return DATA_LOCATION, nil |  | ||||||
| 	case "DATA_GENDER": |  | ||||||
| 		return DATA_GENDER, nil |  | ||||||
| 	case "DATA_OFFERINGS": |  | ||||||
| 		return DATA_OFFERINGS, nil |  | ||||||
| 
 |  | ||||||
| 	default: |  | ||||||
| 		return 0, errors.New("invalid DataTyp string") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ToBytes converts DataTyp or int to a byte slice
 |  | ||||||
| func ToBytes[T ~uint16 | int](value T) []byte { |  | ||||||
| 	bytes := make([]byte, 2) |  | ||||||
| 	binary.BigEndian.PutUint16(bytes, uint16(value)) |  | ||||||
| 	return bytes |  | ||||||
| } |  | ||||||
| @ -1,31 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"encoding/hex" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func NormalizeHex(s string) (string, error) { |  | ||||||
| 	if len(s) >= 2 { |  | ||||||
| 		if s[:2] == "0x" { |  | ||||||
| 			s = s[2:] |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	r, err := hex.DecodeString(s) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
| 	return hex.EncodeToString(r), nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func IsSameHex(left string, right string) bool { |  | ||||||
| 	bl, err := NormalizeHex(left) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	br, err := NormalizeHex(left) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	return strings.Compare(bl, br) == 0 |  | ||||||
| } |  | ||||||
| @ -1,37 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"regexp" |  | ||||||
| 
 |  | ||||||
| 	"golang.org/x/crypto/bcrypt" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	// Define the regex pattern as a constant
 |  | ||||||
| 	pinPattern = `^\d{4}$` |  | ||||||
| 
 |  | ||||||
| 	//Allowed incorrect  PIN attempts
 |  | ||||||
| 	AllowedPINAttempts = uint8(3) |  | ||||||
| 	 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // checks whether the given input is a 4 digit number
 |  | ||||||
| func IsValidPIN(pin string) bool { |  | ||||||
| 	match, _ := regexp.MatchString(pinPattern, pin) |  | ||||||
| 	return match |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // HashPIN uses bcrypt with 8 salt rounds to hash the PIN
 |  | ||||||
| func HashPIN(pin string) (string, error) { |  | ||||||
| 	hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
| 	return string(hash), nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // VerifyPIN compareS the hashed PIN with the plaintext PIN
 |  | ||||||
| func VerifyPIN(hashedPIN, pin string) bool { |  | ||||||
| 	err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin)) |  | ||||||
| 	return err == nil |  | ||||||
| } |  | ||||||
| @ -1,173 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"golang.org/x/crypto/bcrypt" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestIsValidPIN(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name     string |  | ||||||
| 		pin      string |  | ||||||
| 		expected bool |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name:     "Valid PIN with 4 digits", |  | ||||||
| 			pin:      "1234", |  | ||||||
| 			expected: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:     "Valid PIN with leading zeros", |  | ||||||
| 			pin:      "0001", |  | ||||||
| 			expected: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:     "Invalid PIN with less than 4 digits", |  | ||||||
| 			pin:      "123", |  | ||||||
| 			expected: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:     "Invalid PIN with more than 4 digits", |  | ||||||
| 			pin:      "12345", |  | ||||||
| 			expected: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:     "Invalid PIN with letters", |  | ||||||
| 			pin:      "abcd", |  | ||||||
| 			expected: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:     "Invalid PIN with special characters", |  | ||||||
| 			pin:      "12@#", |  | ||||||
| 			expected: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:     "Empty PIN", |  | ||||||
| 			pin:      "", |  | ||||||
| 			expected: false, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			actual := IsValidPIN(tt.pin) |  | ||||||
| 			if actual != tt.expected { |  | ||||||
| 				t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestHashPIN(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name string |  | ||||||
| 		pin  string |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name: "Valid PIN with 4 digits", |  | ||||||
| 			pin:  "1234", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Valid PIN with leading zeros", |  | ||||||
| 			pin:  "0001", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Empty PIN", |  | ||||||
| 			pin:  "", |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			hashedPIN, err := HashPIN(tt.pin) |  | ||||||
| 			if err != nil { |  | ||||||
| 				t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if hashedPIN == "" { |  | ||||||
| 				t.Errorf("HashPIN(%q) returned an empty hash", tt.pin) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			// Ensure the hash can be verified with bcrypt
 |  | ||||||
| 			err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin)) |  | ||||||
| 			if tt.pin != "" && err != nil { |  | ||||||
| 				t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestVerifyMigratedHashPin(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		pin  string |  | ||||||
| 		hash string |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			pin:  "1234", |  | ||||||
| 			hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6", |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.pin, func(t *testing.T) { |  | ||||||
| 			ok := VerifyPIN(tt.hash, tt.pin) |  | ||||||
| 			if !ok { |  | ||||||
| 				t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestVerifyPIN(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name       string |  | ||||||
| 		pin        string |  | ||||||
| 		hashedPIN  string |  | ||||||
| 		shouldPass bool |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name:       "Valid PIN verification", |  | ||||||
| 			pin:        "1234", |  | ||||||
| 			hashedPIN:  hashPINHelper("1234"), |  | ||||||
| 			shouldPass: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:       "Invalid PIN verification with incorrect PIN", |  | ||||||
| 			pin:        "5678", |  | ||||||
| 			hashedPIN:  hashPINHelper("1234"), |  | ||||||
| 			shouldPass: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:       "Invalid PIN verification with empty PIN", |  | ||||||
| 			pin:        "", |  | ||||||
| 			hashedPIN:  hashPINHelper("1234"), |  | ||||||
| 			shouldPass: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:       "Invalid PIN verification with invalid hash", |  | ||||||
| 			pin:        "1234", |  | ||||||
| 			hashedPIN:  "invalidhash", |  | ||||||
| 			shouldPass: false, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			result := VerifyPIN(tt.hashedPIN, tt.pin) |  | ||||||
| 			if result != tt.shouldPass { |  | ||||||
| 				t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Helper function to hash a PIN for testing purposes
 |  | ||||||
| func hashPINHelper(pin string) string { |  | ||||||
| 	hashedPIN, err := HashPIN(pin) |  | ||||||
| 	if err != nil { |  | ||||||
| 		panic("Failed to hash PIN for test setup: " + err.Error()) |  | ||||||
| 	} |  | ||||||
| 	return hashedPIN |  | ||||||
| } |  | ||||||
| @ -1,73 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"regexp" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Define the regex patterns as constants
 |  | ||||||
| const ( |  | ||||||
| 	phoneRegex   = `^(?:\+254|254|0)?((?:7[0-9]{8})|(?:1[01][0-9]{7}))$` |  | ||||||
| 	addressRegex = `^0x[a-fA-F0-9]{40}$` |  | ||||||
| 	aliasRegex   = `^[a-zA-Z0-9]+$` |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // IsValidPhoneNumber checks if the given number is a valid phone number
 |  | ||||||
| func IsValidPhoneNumber(phonenumber string) bool { |  | ||||||
| 	match, _ := regexp.MatchString(phoneRegex, phonenumber) |  | ||||||
| 	return match |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // IsValidAddress checks if the given address is a valid Ethereum address
 |  | ||||||
| func IsValidAddress(address string) bool { |  | ||||||
| 	match, _ := regexp.MatchString(addressRegex, address) |  | ||||||
| 	return match |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // IsValidAlias checks if the alias is a valid alias format
 |  | ||||||
| func IsValidAlias(alias string) bool { |  | ||||||
| 	match, _ := regexp.MatchString(aliasRegex, alias) |  | ||||||
| 	return match |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // CheckRecipient validates the recipient format based on the criteria
 |  | ||||||
| func CheckRecipient(recipient string) (string, error) { |  | ||||||
| 	if IsValidPhoneNumber(recipient) { |  | ||||||
| 		return "phone number", nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if IsValidAddress(recipient) { |  | ||||||
| 		return "address", nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if IsValidAlias(recipient) { |  | ||||||
| 		return "alias", nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return "", fmt.Errorf("invalid recipient: must be a phone number, address or alias") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // FormatPhoneNumber formats a Kenyan phone number to "+254xxxxxxxx".
 |  | ||||||
| func FormatPhoneNumber(phone string) (string, error) { |  | ||||||
| 	if !IsValidPhoneNumber(phone) { |  | ||||||
| 		return "", errors.New("invalid phone number") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Remove any leading "+" and spaces
 |  | ||||||
| 	phone = strings.TrimPrefix(phone, "+") |  | ||||||
| 	phone = strings.ReplaceAll(phone, " ", "") |  | ||||||
| 
 |  | ||||||
| 	// Replace leading "0" with "254" if present
 |  | ||||||
| 	if strings.HasPrefix(phone, "0") { |  | ||||||
| 		phone = "254" + phone[1:] |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Add "+" if not already present
 |  | ||||||
| 	if !strings.HasPrefix(phone, "254") { |  | ||||||
| 		return "", errors.New("unexpected format") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return "+" + phone, nil |  | ||||||
| } |  | ||||||
| @ -1,49 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| 	"git.defalsify.org/vise.git/resource" |  | ||||||
| 	"git.defalsify.org/vise.git/persist" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| 	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func StoreToDb(store *UserDataStore) db.Db { |  | ||||||
| 	return store.Db |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb { |  | ||||||
| 	return dbstorage.NewSubPrefixDb(store.Db, pfx)	 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type StorageServices interface { |  | ||||||
| 	GetPersister(ctx context.Context) (*persist.Persister, error) |  | ||||||
| 	GetUserdataDb(ctx context.Context) (db.Db, error) |  | ||||||
| 	GetResource(ctx context.Context) (resource.Resource, error) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type StorageService struct { |  | ||||||
| 	svc *storage.MenuStorageService |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewStorageService(conn storage.ConnData) (*StorageService, error) { |  | ||||||
| 	svc := &StorageService{ |  | ||||||
| 		svc: storage.NewMenuStorageService(conn, ""), |  | ||||||
| 	} |  | ||||||
| 	return svc, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { |  | ||||||
| 	return ss.svc.GetPersister(ctx) |  | ||||||
| } |  | ||||||
| 	 |  | ||||||
| func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) { |  | ||||||
| 	return ss.svc.GetUserdataDb(ctx) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) { |  | ||||||
| 	return nil, errors.New("not implemented") |  | ||||||
| } |  | ||||||
| @ -1,81 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"math/big" |  | ||||||
| 	"reflect" |  | ||||||
| 	"strconv" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type TransactionData struct { |  | ||||||
| 	TemporaryValue string |  | ||||||
| 	ActiveSym      string |  | ||||||
| 	Amount         string |  | ||||||
| 	PublicKey      string |  | ||||||
| 	Recipient      string |  | ||||||
| 	ActiveDecimal  string |  | ||||||
| 	ActiveAddress  string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) { |  | ||||||
| 	// Parse token decimal
 |  | ||||||
| 	tokenDecimal, err := strconv.Atoi(activeDecimal) |  | ||||||
| 	if err != nil { |  | ||||||
| 
 |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Parse amount
 |  | ||||||
| 	amount, _, err := big.ParseFloat(storedAmount, 10, 0, big.ToZero) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Scale the amount
 |  | ||||||
| 	multiplier := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenDecimal)), nil)) |  | ||||||
| 	finalAmount := new(big.Float).Mul(amount, multiplier) |  | ||||||
| 
 |  | ||||||
| 	// Convert finalAmount to a string
 |  | ||||||
| 	finalAmountStr := new(big.Int) |  | ||||||
| 	finalAmount.Int(finalAmountStr) |  | ||||||
| 
 |  | ||||||
| 	return finalAmountStr.String(), nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func ReadTransactionData(ctx context.Context, store DataStore, sessionId string) (TransactionData, error) { |  | ||||||
| 	data := TransactionData{} |  | ||||||
| 	fieldToKey := map[string]DataTyp{ |  | ||||||
| 		"TemporaryValue": DATA_TEMPORARY_VALUE, |  | ||||||
| 		"ActiveSym":      DATA_ACTIVE_SYM, |  | ||||||
| 		"Amount":         DATA_AMOUNT, |  | ||||||
| 		"PublicKey":      DATA_PUBLIC_KEY, |  | ||||||
| 		"Recipient":      DATA_RECIPIENT, |  | ||||||
| 		"ActiveDecimal":  DATA_ACTIVE_DECIMAL, |  | ||||||
| 		"ActiveAddress":  DATA_ACTIVE_ADDRESS, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	v := reflect.ValueOf(&data).Elem() |  | ||||||
| 	for fieldName, key := range fieldToKey { |  | ||||||
| 		field := v.FieldByName(fieldName) |  | ||||||
| 		if !field.IsValid() || !field.CanSet() { |  | ||||||
| 			return data, errors.New("invalid struct field: " + fieldName) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		value, err := readStringEntry(ctx, store, sessionId, key) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return data, err |  | ||||||
| 		} |  | ||||||
| 		field.SetString(value) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return data, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func readStringEntry(ctx context.Context, store DataStore, sessionId string, key DataTyp) (string, error) { |  | ||||||
| 	entry, err := store.ReadEntry(ctx, sessionId, key) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
| 	return string(entry), nil |  | ||||||
| } |  | ||||||
| @ -1,129 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/alecthomas/assert/v2" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestParseAndScaleAmount(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name        string |  | ||||||
| 		amount      string |  | ||||||
| 		decimals    string |  | ||||||
| 		want        string |  | ||||||
| 		expectError bool |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name:        "whole number", |  | ||||||
| 			amount:      "123", |  | ||||||
| 			decimals:    "2", |  | ||||||
| 			want:        "12300", |  | ||||||
| 			expectError: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "decimal number", |  | ||||||
| 			amount:      "123.45", |  | ||||||
| 			decimals:    "2", |  | ||||||
| 			want:        "12345", |  | ||||||
| 			expectError: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "zero decimals", |  | ||||||
| 			amount:      "123.45", |  | ||||||
| 			decimals:    "0", |  | ||||||
| 			want:        "123", |  | ||||||
| 			expectError: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "large number", |  | ||||||
| 			amount:      "1000000.01", |  | ||||||
| 			decimals:    "6", |  | ||||||
| 			want:        "1000000010000", |  | ||||||
| 			expectError: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "invalid amount", |  | ||||||
| 			amount:      "abc", |  | ||||||
| 			decimals:    "2", |  | ||||||
| 			want:        "", |  | ||||||
| 			expectError: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "invalid decimals", |  | ||||||
| 			amount:      "123.45", |  | ||||||
| 			decimals:    "abc", |  | ||||||
| 			want:        "", |  | ||||||
| 			expectError: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "zero amount", |  | ||||||
| 			amount:      "0", |  | ||||||
| 			decimals:    "2", |  | ||||||
| 			want:        "0", |  | ||||||
| 			expectError: false, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			got, err := ParseAndScaleAmount(tt.amount, tt.decimals) |  | ||||||
| 
 |  | ||||||
| 			// Check error cases
 |  | ||||||
| 			if tt.expectError { |  | ||||||
| 				if err == nil { |  | ||||||
| 					t.Errorf("ParseAndScaleAmount(%q, %q) expected error, got nil", tt.amount, tt.decimals) |  | ||||||
| 				} |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if err != nil { |  | ||||||
| 				t.Errorf("ParseAndScaleAmount(%q, %q) unexpected error: %v", tt.amount, tt.decimals, err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if got != tt.want { |  | ||||||
| 				t.Errorf("ParseAndScaleAmount(%q, %q) = %v, want %v", tt.amount, tt.decimals, got, tt.want) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestReadTransactionData(t *testing.T) { |  | ||||||
| 	sessionId := "session123" |  | ||||||
| 	publicKey := "0X13242618721" |  | ||||||
| 	ctx, store := InitializeTestDb(t) |  | ||||||
| 
 |  | ||||||
| 	// Test transaction data
 |  | ||||||
| 	transactionData := map[DataTyp]string{ |  | ||||||
| 		DATA_TEMPORARY_VALUE: "0712345678", |  | ||||||
| 		DATA_ACTIVE_SYM:      "SRF", |  | ||||||
| 		DATA_AMOUNT:          "1000000", |  | ||||||
| 		DATA_PUBLIC_KEY:      publicKey, |  | ||||||
| 		DATA_RECIPIENT:       "0x41c188d63Qa", |  | ||||||
| 		DATA_ACTIVE_DECIMAL:  "6", |  | ||||||
| 		DATA_ACTIVE_ADDRESS:  "0xd4c288865Ce", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Store the data
 |  | ||||||
| 	for key, value := range transactionData { |  | ||||||
| 		if err := store.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil { |  | ||||||
| 			t.Fatal(err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	expectedResult := TransactionData{ |  | ||||||
| 		TemporaryValue: "0712345678", |  | ||||||
| 		ActiveSym:      "SRF", |  | ||||||
| 		Amount:         "1000000", |  | ||||||
| 		PublicKey:      publicKey, |  | ||||||
| 		Recipient:      "0x41c188d63Qa", |  | ||||||
| 		ActiveDecimal:  "6", |  | ||||||
| 		ActiveAddress:  "0xd4c288865Ce", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	data, err := ReadTransactionData(ctx, store, sessionId) |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	assert.Equal(t, expectedResult, data) |  | ||||||
| } |  | ||||||
| @ -1,119 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db" |  | ||||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // TransferMetadata helps organize data fields
 |  | ||||||
| type TransferMetadata struct { |  | ||||||
| 	Senders        string |  | ||||||
| 	Recipients     string |  | ||||||
| 	TransferValues string |  | ||||||
| 	Addresses      string |  | ||||||
| 	TxHashes       string |  | ||||||
| 	Dates          string |  | ||||||
| 	Symbols        string |  | ||||||
| 	Decimals       string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ProcessTransfers converts transfers into formatted strings
 |  | ||||||
| func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetadata { |  | ||||||
| 	var data TransferMetadata |  | ||||||
| 	var senders, recipients, transferValues, addresses, txHashes, dates, symbols, decimals []string |  | ||||||
| 
 |  | ||||||
| 	for _, t := range transfers { |  | ||||||
| 		senders = append(senders, t.Sender) |  | ||||||
| 		recipients = append(recipients, t.Recipient) |  | ||||||
| 
 |  | ||||||
| 		// Scale down the amount
 |  | ||||||
| 		scaledBalance := ScaleDownBalance(t.TransferValue, t.TokenDecimals) |  | ||||||
| 		transferValues = append(transferValues, scaledBalance) |  | ||||||
| 
 |  | ||||||
| 		addresses = append(addresses, t.ContractAddress) |  | ||||||
| 		txHashes = append(txHashes, t.TxHash) |  | ||||||
| 		dates = append(dates, fmt.Sprintf("%s", t.DateBlock)) |  | ||||||
| 		symbols = append(symbols, t.TokenSymbol) |  | ||||||
| 		decimals = append(decimals, t.TokenDecimals) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	data.Senders = strings.Join(senders, "\n") |  | ||||||
| 	data.Recipients = strings.Join(recipients, "\n") |  | ||||||
| 	data.TransferValues = strings.Join(transferValues, "\n") |  | ||||||
| 	data.Addresses = strings.Join(addresses, "\n") |  | ||||||
| 	data.TxHashes = strings.Join(txHashes, "\n") |  | ||||||
| 	data.Dates = strings.Join(dates, "\n") |  | ||||||
| 	data.Symbols = strings.Join(symbols, "\n") |  | ||||||
| 	data.Decimals = strings.Join(decimals, "\n") |  | ||||||
| 
 |  | ||||||
| 	return data |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetTransferData retrieves and matches transfer data
 |  | ||||||
| // returns a formatted string of the full transaction/statement
 |  | ||||||
| func GetTransferData(ctx context.Context, db dbstorage.PrefixDb, publicKey string, index int) (string, error) { |  | ||||||
| 	keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS} |  | ||||||
| 	data := make(map[DataTyp]string) |  | ||||||
| 
 |  | ||||||
| 	for _, key := range keys { |  | ||||||
| 		value, err := db.Get(ctx, ToBytes(key)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return "", fmt.Errorf("failed to get %s: %v", ToBytes(key), err) |  | ||||||
| 		} |  | ||||||
| 		data[key] = string(value) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Split the data
 |  | ||||||
| 	senders := strings.Split(string(data[DATA_TX_SENDERS]), "\n") |  | ||||||
| 	recipients := strings.Split(string(data[DATA_TX_RECIPIENTS]), "\n") |  | ||||||
| 	values := strings.Split(string(data[DATA_TX_VALUES]), "\n") |  | ||||||
| 	addresses := strings.Split(string(data[DATA_TX_ADDRESSES]), "\n") |  | ||||||
| 	hashes := strings.Split(string(data[DATA_TX_HASHES]), "\n") |  | ||||||
| 	dates := strings.Split(string(data[DATA_TX_DATES]), "\n") |  | ||||||
| 	syms := strings.Split(string(data[DATA_TX_SYMBOLS]), "\n") |  | ||||||
| 
 |  | ||||||
| 	// Check if index is within range
 |  | ||||||
| 	if index < 1 || index > len(senders) { |  | ||||||
| 		return "", fmt.Errorf("transaction not found: index %d out of range", index) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Adjust for 0-based indexing
 |  | ||||||
| 	i := index - 1 |  | ||||||
| 	transactionType := "Received" |  | ||||||
| 	party := fmt.Sprintf("From: %s", strings.TrimSpace(senders[i])) |  | ||||||
| 	if strings.TrimSpace(senders[i]) == publicKey { |  | ||||||
| 		transactionType = "Sent" |  | ||||||
| 		party = fmt.Sprintf("To: %s", strings.TrimSpace(recipients[i])) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	formattedDate := formatDate(strings.TrimSpace(dates[i])) |  | ||||||
| 
 |  | ||||||
| 	// Build the full transaction detail
 |  | ||||||
| 	detail := fmt.Sprintf( |  | ||||||
| 		"%s %s %s\n%s\nContract address: %s\nTxhash: %s\nDate: %s", |  | ||||||
| 		transactionType, |  | ||||||
| 		strings.TrimSpace(values[i]), |  | ||||||
| 		strings.TrimSpace(syms[i]), |  | ||||||
| 		party, |  | ||||||
| 		strings.TrimSpace(addresses[i]), |  | ||||||
| 		strings.TrimSpace(hashes[i]), |  | ||||||
| 		formattedDate, |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	return detail, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Helper function to format date in desired output
 |  | ||||||
| func formatDate(dateStr string) string { |  | ||||||
| 	parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Println("Error parsing date:", err) |  | ||||||
| 		return "" |  | ||||||
| 	} |  | ||||||
| 	return parsedDate.Format("2006-01-02 03:04:05 PM") |  | ||||||
| } |  | ||||||
| @ -1,174 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"math/big" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db" |  | ||||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // VoucherMetadata helps organize data fields
 |  | ||||||
| type VoucherMetadata struct { |  | ||||||
| 	Symbols   string |  | ||||||
| 	Balances  string |  | ||||||
| 	Decimals  string |  | ||||||
| 	Addresses string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ProcessVouchers converts holdings into formatted strings
 |  | ||||||
| func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata { |  | ||||||
| 	var data VoucherMetadata |  | ||||||
| 	var symbols, balances, decimals, addresses []string |  | ||||||
| 
 |  | ||||||
| 	for i, h := range holdings { |  | ||||||
| 		symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol)) |  | ||||||
| 
 |  | ||||||
| 		// Scale down the balance
 |  | ||||||
| 		scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals) |  | ||||||
| 
 |  | ||||||
| 		balances = append(balances, fmt.Sprintf("%d:%s", i+1, scaledBalance)) |  | ||||||
| 		decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals)) |  | ||||||
| 		addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	data.Symbols = strings.Join(symbols, "\n") |  | ||||||
| 	data.Balances = strings.Join(balances, "\n") |  | ||||||
| 	data.Decimals = strings.Join(decimals, "\n") |  | ||||||
| 	data.Addresses = strings.Join(addresses, "\n") |  | ||||||
| 
 |  | ||||||
| 	return data |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func ScaleDownBalance(balance, decimals string) string { |  | ||||||
| 	// Convert balance and decimals to big.Float
 |  | ||||||
| 	bal := new(big.Float) |  | ||||||
| 	bal.SetString(balance) |  | ||||||
| 
 |  | ||||||
| 	dec, ok := new(big.Int).SetString(decimals, 10) |  | ||||||
| 	if !ok { |  | ||||||
| 		dec = big.NewInt(0) // Default to 0 decimals in case of conversion failure
 |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), dec, nil)) |  | ||||||
| 	scaledBalance := new(big.Float).Quo(bal, divisor) |  | ||||||
| 
 |  | ||||||
| 	// Return the scaled balance without trailing decimals if it's an integer
 |  | ||||||
| 	if scaledBalance.IsInt() { |  | ||||||
| 		return scaledBalance.Text('f', 0) |  | ||||||
| 	} |  | ||||||
| 	return scaledBalance.Text('f', -1) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetVoucherData retrieves and matches voucher data
 |  | ||||||
| func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { |  | ||||||
| 	keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES} |  | ||||||
| 	data := make(map[DataTyp]string) |  | ||||||
| 
 |  | ||||||
| 	for _, key := range keys { |  | ||||||
| 		value, err := db.Get(ctx, ToBytes(key)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, fmt.Errorf("failed to get %s: %v", ToBytes(key), err) |  | ||||||
| 		} |  | ||||||
| 		data[key] = string(value) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	symbol, balance, decimal, address := MatchVoucher(input, |  | ||||||
| 		data[DATA_VOUCHER_SYMBOLS], |  | ||||||
| 		data[DATA_VOUCHER_BALANCES], |  | ||||||
| 		data[DATA_VOUCHER_DECIMALS], |  | ||||||
| 		data[DATA_VOUCHER_ADDRESSES], |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 	if symbol == "" { |  | ||||||
| 		return nil, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return &dataserviceapi.TokenHoldings{ |  | ||||||
| 		TokenSymbol:     string(symbol), |  | ||||||
| 		Balance:         string(balance), |  | ||||||
| 		TokenDecimals:   string(decimal), |  | ||||||
| 		ContractAddress: string(address), |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input.
 |  | ||||||
| func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) { |  | ||||||
| 	symList := strings.Split(symbols, "\n") |  | ||||||
| 	balList := strings.Split(balances, "\n") |  | ||||||
| 	decList := strings.Split(decimals, "\n") |  | ||||||
| 	addrList := strings.Split(addresses, "\n") |  | ||||||
| 
 |  | ||||||
| 	logg.Tracef("found", "symlist", symList, "syms", symbols, "input", input) |  | ||||||
| 	for i, sym := range symList { |  | ||||||
| 		parts := strings.SplitN(sym, ":", 2) |  | ||||||
| 
 |  | ||||||
| 		if input == parts[0] || strings.EqualFold(input, parts[1]) { |  | ||||||
| 			symbol = parts[1] |  | ||||||
| 			if i < len(balList) { |  | ||||||
| 				balance = strings.SplitN(balList[i], ":", 2)[1] |  | ||||||
| 			} |  | ||||||
| 			if i < len(decList) { |  | ||||||
| 				decimal = strings.SplitN(decList[i], ":", 2)[1] |  | ||||||
| 			} |  | ||||||
| 			if i < len(addrList) { |  | ||||||
| 				address = strings.SplitN(addrList[i], ":", 2)[1] |  | ||||||
| 			} |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore.
 |  | ||||||
| func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { |  | ||||||
| 	tempData := fmt.Sprintf("%s,%s,%s,%s", data.TokenSymbol, data.Balance, data.TokenDecimals, data.ContractAddress) |  | ||||||
| 
 |  | ||||||
| 	if err := store.WriteEntry(ctx, sessionId, DATA_TEMPORARY_VALUE, []byte(tempData)); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetTemporaryVoucherData retrieves temporary voucher metadata from the DataStore.
 |  | ||||||
| func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) { |  | ||||||
| 	temp_data, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	values := strings.SplitN(string(temp_data), ",", 4) |  | ||||||
| 
 |  | ||||||
| 	data := &dataserviceapi.TokenHoldings{} |  | ||||||
| 
 |  | ||||||
| 	data.TokenSymbol = values[0] |  | ||||||
| 	data.Balance = values[1] |  | ||||||
| 	data.TokenDecimals = values[2] |  | ||||||
| 	data.ContractAddress = values[3] |  | ||||||
| 
 |  | ||||||
| 	return data, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // UpdateVoucherData updates the active voucher data in the DataStore.
 |  | ||||||
| func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { |  | ||||||
| 	logg.TraceCtxf(ctx, "dtal", "data", data) |  | ||||||
| 	// Active voucher data entries
 |  | ||||||
| 	activeEntries := map[DataTyp][]byte{ |  | ||||||
| 		DATA_ACTIVE_SYM:     []byte(data.TokenSymbol), |  | ||||||
| 		DATA_ACTIVE_BAL:     []byte(data.Balance), |  | ||||||
| 		DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals), |  | ||||||
| 		DATA_ACTIVE_ADDRESS: []byte(data.ContractAddress), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Write active data
 |  | ||||||
| 	for key, value := range activeEntries { |  | ||||||
| 		if err := store.WriteEntry(ctx, sessionId, key, value); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @ -1,200 +0,0 @@ | |||||||
| package common |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"github.com/alecthomas/assert/v2" |  | ||||||
| 	"github.com/stretchr/testify/require" |  | ||||||
| 
 |  | ||||||
| 	visedb "git.defalsify.org/vise.git/db" |  | ||||||
| 	memdb "git.defalsify.org/vise.git/db/mem" |  | ||||||
| 	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db" |  | ||||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // InitializeTestDb sets up and returns an in-memory database and store.
 |  | ||||||
| func InitializeTestDb(t *testing.T) (context.Context, *UserDataStore) { |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 
 |  | ||||||
| 	// Initialize memDb
 |  | ||||||
| 	db := memdb.NewMemDb() |  | ||||||
| 	err := db.Connect(ctx, "") |  | ||||||
| 	require.NoError(t, err, "Failed to connect to memDb") |  | ||||||
| 
 |  | ||||||
| 	// Create UserDataStore with memDb
 |  | ||||||
| 	store := &UserDataStore{Db: db} |  | ||||||
| 
 |  | ||||||
| 	t.Cleanup(func() { |  | ||||||
| 		db.Close() // Ensure the DB is closed after each test
 |  | ||||||
| 	}) |  | ||||||
| 
 |  | ||||||
| 	return ctx, store |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMatchVoucher(t *testing.T) { |  | ||||||
| 	symbols := "1:SRF\n2:MILO" |  | ||||||
| 	balances := "1:100\n2:200" |  | ||||||
| 	decimals := "1:6\n2:4" |  | ||||||
| 	addresses := "1:0xd4c288865Ce\n2:0x41c188d63Qa" |  | ||||||
| 
 |  | ||||||
| 	// Test for valid voucher
 |  | ||||||
| 	symbol, balance, decimal, address := MatchVoucher("2", symbols, balances, decimals, addresses) |  | ||||||
| 
 |  | ||||||
| 	// Assertions for valid voucher
 |  | ||||||
| 	assert.Equal(t, "MILO", symbol) |  | ||||||
| 	assert.Equal(t, "200", balance) |  | ||||||
| 	assert.Equal(t, "4", decimal) |  | ||||||
| 	assert.Equal(t, "0x41c188d63Qa", address) |  | ||||||
| 
 |  | ||||||
| 	// Test for non-existent voucher
 |  | ||||||
| 	symbol, balance, decimal, address = MatchVoucher("3", symbols, balances, decimals, addresses) |  | ||||||
| 
 |  | ||||||
| 	// Assertions for non-match
 |  | ||||||
| 	assert.Equal(t, "", symbol) |  | ||||||
| 	assert.Equal(t, "", balance) |  | ||||||
| 	assert.Equal(t, "", decimal) |  | ||||||
| 	assert.Equal(t, "", address) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestProcessVouchers(t *testing.T) { |  | ||||||
| 	holdings := []dataserviceapi.TokenHoldings{ |  | ||||||
| 		{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100000000"}, |  | ||||||
| 		{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200000000"}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	expectedResult := VoucherMetadata{ |  | ||||||
| 		Symbols:   "1:SRF\n2:MILO", |  | ||||||
| 		Balances:  "1:100\n2:20000", |  | ||||||
| 		Decimals:  "1:6\n2:4", |  | ||||||
| 		Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	result := ProcessVouchers(holdings) |  | ||||||
| 
 |  | ||||||
| 	assert.Equal(t, expectedResult, result) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetVoucherData(t *testing.T) { |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 
 |  | ||||||
| 	db := memdb.NewMemDb() |  | ||||||
| 	err := db.Connect(ctx, "") |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	prefix := ToBytes(visedb.DATATYPE_USERDATA) |  | ||||||
| 	spdb := dbstorage.NewSubPrefixDb(db, prefix) |  | ||||||
| 
 |  | ||||||
| 	// Test voucher data
 |  | ||||||
| 	mockData := map[DataTyp][]byte{ |  | ||||||
| 		DATA_VOUCHER_SYMBOLS:   []byte("1:SRF\n2:MILO"), |  | ||||||
| 		DATA_VOUCHER_BALANCES:  []byte("1:100\n2:200"), |  | ||||||
| 		DATA_VOUCHER_DECIMALS:  []byte("1:6\n2:4"), |  | ||||||
| 		DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Put the data
 |  | ||||||
| 	for key, value := range mockData { |  | ||||||
| 		err = spdb.Put(ctx, []byte(ToBytes(key)), []byte(value)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			t.Fatal(err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	result, err := GetVoucherData(ctx, spdb, "1") |  | ||||||
| 
 |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	assert.Equal(t, "SRF", result.TokenSymbol) |  | ||||||
| 	assert.Equal(t, "100", result.Balance) |  | ||||||
| 	assert.Equal(t, "6", result.TokenDecimals) |  | ||||||
| 	assert.Equal(t, "0xd4c288865Ce", result.ContractAddress) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestStoreTemporaryVoucher(t *testing.T) { |  | ||||||
| 	ctx, store := InitializeTestDb(t) |  | ||||||
| 	sessionId := "session123" |  | ||||||
| 
 |  | ||||||
| 	// Test data
 |  | ||||||
| 	voucherData := &dataserviceapi.TokenHoldings{ |  | ||||||
| 		TokenSymbol:     "SRF", |  | ||||||
| 		Balance:         "200", |  | ||||||
| 		TokenDecimals:   "6", |  | ||||||
| 		ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Execute the function being tested
 |  | ||||||
| 	err := StoreTemporaryVoucher(ctx, store, sessionId, voucherData) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	// Verify stored data
 |  | ||||||
| 	expectedData := fmt.Sprintf("%s,%s,%s,%s", "SRF", "200", "6", "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9") |  | ||||||
| 
 |  | ||||||
| 	storedValue, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	require.Equal(t, expectedData, string(storedValue), "Mismatch for key %v", DATA_TEMPORARY_VALUE) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGetTemporaryVoucherData(t *testing.T) { |  | ||||||
| 	ctx, store := InitializeTestDb(t) |  | ||||||
| 	sessionId := "session123" |  | ||||||
| 
 |  | ||||||
| 	// Test voucher data
 |  | ||||||
| 	tempData := &dataserviceapi.TokenHoldings{ |  | ||||||
| 		TokenSymbol:     "SRF", |  | ||||||
| 		Balance:         "200", |  | ||||||
| 		TokenDecimals:   "6", |  | ||||||
| 		ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Store the data
 |  | ||||||
| 	err := StoreTemporaryVoucher(ctx, store, sessionId, tempData) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	// Execute the function being tested
 |  | ||||||
| 	data, err := GetTemporaryVoucherData(ctx, store, sessionId) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	require.Equal(t, tempData, data) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestUpdateVoucherData(t *testing.T) { |  | ||||||
| 	ctx, store := InitializeTestDb(t) |  | ||||||
| 	sessionId := "session123" |  | ||||||
| 
 |  | ||||||
| 	// New voucher data
 |  | ||||||
| 	newData := &dataserviceapi.TokenHoldings{ |  | ||||||
| 		TokenSymbol:     "SRF", |  | ||||||
| 		Balance:         "200", |  | ||||||
| 		TokenDecimals:   "6", |  | ||||||
| 		ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Old temporary data
 |  | ||||||
| 	tempData := &dataserviceapi.TokenHoldings{ |  | ||||||
| 		TokenSymbol:     "OLD", |  | ||||||
| 		Balance:         "100", |  | ||||||
| 		TokenDecimals:   "8", |  | ||||||
| 		ContractAddress: "0xold", |  | ||||||
| 	} |  | ||||||
| 	require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData)) |  | ||||||
| 
 |  | ||||||
| 	// Execute update
 |  | ||||||
| 	err := UpdateVoucherData(ctx, store, sessionId, newData) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 |  | ||||||
| 	// Verify active data was stored correctly
 |  | ||||||
| 	activeEntries := map[DataTyp][]byte{ |  | ||||||
| 		DATA_ACTIVE_SYM:     []byte(newData.TokenSymbol), |  | ||||||
| 		DATA_ACTIVE_BAL:     []byte(newData.Balance), |  | ||||||
| 		DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals), |  | ||||||
| 		DATA_ACTIVE_ADDRESS: []byte(newData.ContractAddress), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for key, expectedValue := range activeEntries { |  | ||||||
| 		storedValue, err := store.ReadEntry(ctx, sessionId, key) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		require.Equal(t, expectedValue, storedValue, "Active data mismatch for key %v", key) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										113
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								config/config.go
									
									
									
									
									
								
							| @ -1,117 +1,10 @@ | |||||||
| package config | package config | ||||||
| 
 | 
 | ||||||
| import ( |  | ||||||
| 	"net/url" |  | ||||||
| 	"strings" |  | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| ) |  | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	createAccountPath          = "/api/v2/account/create" |     CreateAccountURL = "https://custodial.sarafu.africa/api/account/create" | ||||||
| 	trackStatusPath            = "/api/track" |     TrackStatusURL   = "https://custodial.sarafu.africa/api/track/" | ||||||
| 	balancePathPrefix          = "/api/account" |     BalanceURL       = "https://custodial.sarafu.africa/api/account/status/" | ||||||
| 	trackPath                  = "/api/v2/account/status" |  | ||||||
| 	tokenTransferPrefix        = "/api/v2/token/transfer" |  | ||||||
| 	voucherHoldingsPathPrefix  = "/api/v1/holdings" |  | ||||||
| 	voucherTransfersPathPrefix = "/api/v1/transfers/last10" |  | ||||||
| 	voucherDataPathPrefix      = "/api/v1/token" |  | ||||||
| 	AliasPrefix                = "api/v1/alias" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( |  | ||||||
| 	defaultLanguage		   = "eng" |  | ||||||
| 	languages []string |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	custodialURLBase string |  | ||||||
| 	dataURLBase      string |  | ||||||
| 	BearerToken      string |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	CreateAccountURL    string |  | ||||||
| 	TrackStatusURL      string |  | ||||||
| 	BalanceURL          string |  | ||||||
| 	TrackURL            string |  | ||||||
| 	TokenTransferURL    string |  | ||||||
| 	VoucherHoldingsURL  string |  | ||||||
| 	VoucherTransfersURL string |  | ||||||
| 	VoucherDataURL      string |  | ||||||
| 	CheckAliasURL       string |  | ||||||
| 	DbConn		string |  | ||||||
| 	DefaultLanguage	    string |  | ||||||
| 	Languages	[]string |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func setLanguage() error { |  | ||||||
| 	defaultLanguage = initializers.GetEnv("DEFAULT_LANGUAGE", defaultLanguage) |  | ||||||
| 	languages = strings.Split(initializers.GetEnv("LANGUAGES", defaultLanguage), ",") |  | ||||||
| 	haveDefaultLanguage := false |  | ||||||
| 	for i, v := range(languages) { |  | ||||||
| 		languages[i] = strings.ReplaceAll(v, " ", "") |  | ||||||
| 		if languages[i] == defaultLanguage { |  | ||||||
| 			haveDefaultLanguage = true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if !haveDefaultLanguage { |  | ||||||
| 		languages = append([]string{defaultLanguage}, languages...) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func setBase() error { |  | ||||||
| 	var err error |  | ||||||
| 
 |  | ||||||
| 	custodialURLBase = initializers.GetEnv("CUSTODIAL_URL_BASE", "http://localhost:5003") |  | ||||||
| 	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006") |  | ||||||
| 	BearerToken = initializers.GetEnv("BEARER_TOKEN", "") |  | ||||||
| 
 |  | ||||||
| 	_, err = url.Parse(custodialURLBase) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	_, err = url.Parse(dataURLBase) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func setConn() error { |  | ||||||
| 	DbConn = initializers.GetEnv("DB_CONN", "") |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // LoadConfig initializes the configuration values after environment variables are loaded.
 |  | ||||||
| func LoadConfig() error { |  | ||||||
| 	err := setBase() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	err = setConn() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	err = setLanguage() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath) |  | ||||||
| 	TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath) |  | ||||||
| 	BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix) |  | ||||||
| 	TrackURL, _ = url.JoinPath(custodialURLBase, trackPath) |  | ||||||
| 	TokenTransferURL, _ = url.JoinPath(custodialURLBase, tokenTransferPrefix) |  | ||||||
| 	VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix) |  | ||||||
| 	VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) |  | ||||||
| 	VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) |  | ||||||
| 	CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix) |  | ||||||
| 	DefaultLanguage = defaultLanguage |  | ||||||
| 	Languages = languages |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,5 +0,0 @@ | |||||||
| package debug |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	DebugCap uint32 |  | ||||||
| ) |  | ||||||
							
								
								
									
										84
									
								
								debug/db.go
									
									
									
									
									
								
							
							
						
						
									
										84
									
								
								debug/db.go
									
									
									
									
									
								
							| @ -1,84 +0,0 @@ | |||||||
| package debug |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"encoding/binary" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/common" |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	dbTypStr map[common.DataTyp]string = make(map[common.DataTyp]string) |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type KeyInfo struct { |  | ||||||
| 	SessionId string |  | ||||||
| 	Typ uint8 |  | ||||||
| 	SubTyp common.DataTyp |  | ||||||
| 	Label string |  | ||||||
| 	Description string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (k KeyInfo) String() string { |  | ||||||
| 	v := uint16(k.SubTyp) |  | ||||||
| 	s := subTypToString(k.SubTyp) |  | ||||||
| 	if s == "" { |  | ||||||
| 		v = uint16(k.Typ) |  | ||||||
| 		s = typToString(k.Typ) |  | ||||||
| 	} |  | ||||||
| 	return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) { |  | ||||||
| 	o := KeyInfo{} |  | ||||||
| 	b := []byte(sessionId) |  | ||||||
| 
 |  | ||||||
| 	if len(k) <= len(b) { |  | ||||||
| 		return o, fmt.Errorf("storage key missing") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	o.SessionId = sessionId |  | ||||||
| 
 |  | ||||||
| 	o.Typ = uint8(k[0]) |  | ||||||
| 	k = k[1:] |  | ||||||
| 	o.SessionId = string(k[:len(b)]) |  | ||||||
| 	k = k[len(b):] |  | ||||||
| 
 |  | ||||||
| 	if o.Typ == db.DATATYPE_USERDATA { |  | ||||||
| 		if len(k) == 0 { |  | ||||||
| 			return o, fmt.Errorf("missing subtype key") |  | ||||||
| 		} |  | ||||||
| 		v := binary.BigEndian.Uint16(k[:2]) |  | ||||||
| 		o.SubTyp = common.DataTyp(v) |  | ||||||
| 		o.Label = subTypToString(o.SubTyp) |  | ||||||
| 		k = k[2:] |  | ||||||
| 	} else { |  | ||||||
| 		o.Label = typToString(o.Typ) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if len(k) != 0 { |  | ||||||
| 		return o, fmt.Errorf("excess key information") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return o, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func FromKey(k []byte) (KeyInfo, error) { |  | ||||||
| 	o := KeyInfo{} |  | ||||||
| 
 |  | ||||||
| 	if len(k) < 4 { |  | ||||||
| 		return o, fmt.Errorf("insufficient key length") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	sessionIdBytes := k[1:len(k)-2] |  | ||||||
| 	return ToKeyInfo(k, string(sessionIdBytes)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func subTypToString(v common.DataTyp) string { |  | ||||||
| 	return dbTypStr[v + db.DATATYPE_USERDATA + 1] |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func typToString(v uint8) string { |  | ||||||
| 	return dbTypStr[common.DataTyp(uint16(v))] |  | ||||||
| } |  | ||||||
| @ -1,44 +0,0 @@ | |||||||
| // +build debugdb
 |  | ||||||
| 
 |  | ||||||
| package debug |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/common" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	DebugCap |= 1 |  | ||||||
| 	dbTypStr[db.DATATYPE_STATE] = "internal state" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_LOCATION] = "location" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_GENDER] = "gender" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_OFFERINGS] = "offerings" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_RECIPIENT] = "recipient" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_AMOUNT] = "amount" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TEMPORARY_VALUE] = "temporary value" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_SYM] = "active sym" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_BAL] = "active bal" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_BLOCKED_NUMBER] = "blocked number" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_DECIMAL] = "active decimal" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_ADDRESS] = "active address" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_SYMBOLS] = "voucher symbols" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_BALANCES] = "voucher balances" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_DECIMALS] = "voucher decimals" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_ADDRESSES] = "voucher addresses" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SENDERS] = "tx senders" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_RECIPIENTS] = "tx recipients" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_VALUES] = "tx values" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_ADDRESSES] = "tx addresses" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_HASHES] = "tx hashes" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DATES] = "tx dates" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SYMBOLS] = "tx symbols" |  | ||||||
| 	dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DECIMALS] = "tx decimals" |  | ||||||
| } |  | ||||||
| @ -1,78 +0,0 @@ | |||||||
| package debug |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/common" |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestDebugDbSubKeyInfo(t *testing.T) { |  | ||||||
| 	s := "foo" |  | ||||||
| 	b := []byte{0x20} |  | ||||||
| 	b = append(b, []byte(s)...) |  | ||||||
| 	b = append(b, []byte{0x00, 0x02}...) |  | ||||||
| 	r, err := ToKeyInfo(b, s) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	if r.SessionId != s { |  | ||||||
| 		t.Fatalf("expected %s, got %s", s, r.SessionId) |  | ||||||
| 	} |  | ||||||
| 	if r.Typ != 32 { |  | ||||||
| 		t.Fatalf("expected 64, got %d", r.Typ) |  | ||||||
| 	} |  | ||||||
| 	if r.SubTyp != 2 { |  | ||||||
| 		t.Fatalf("expected 2, got %d", r.SubTyp) |  | ||||||
| 	} |  | ||||||
| 	if DebugCap & 1 > 0 { |  | ||||||
| 		if r.Label != "tracking id" { |  | ||||||
| 			t.Fatalf("expected 'tracking id', got '%s'", r.Label) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDebugDbKeyInfo(t *testing.T) { |  | ||||||
| 	s := "bar" |  | ||||||
| 	b := []byte{0x10} |  | ||||||
| 	b = append(b, []byte(s)...) |  | ||||||
| 	r, err := ToKeyInfo(b, s) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	if r.SessionId != s { |  | ||||||
| 		t.Fatalf("expected %s, got %s", s, r.SessionId) |  | ||||||
| 	} |  | ||||||
| 	if r.Typ != 16 { |  | ||||||
| 		t.Fatalf("expected 16, got %d", r.Typ) |  | ||||||
| 	} |  | ||||||
| 	if DebugCap & 1 > 0 { |  | ||||||
| 		if r.Label != "internal state" { |  | ||||||
| 			t.Fatalf("expected 'internal_state', got '%s'", r.Label) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDebugDbKeyInfoRestore(t *testing.T) { |  | ||||||
| 	s := "bar" |  | ||||||
| 	b := []byte{db.DATATYPE_USERDATA} |  | ||||||
| 	b = append(b, []byte(s)...) |  | ||||||
| 	k := common.ToBytes(common.DATA_ACTIVE_SYM) |  | ||||||
| 	b = append(b, k...) |  | ||||||
| 
 |  | ||||||
| 	r, err := ToKeyInfo(b, s) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	if r.SessionId != s { |  | ||||||
| 		t.Fatalf("expected %s, got %s", s, r.SessionId) |  | ||||||
| 	} |  | ||||||
| 	if r.Typ != 32 { |  | ||||||
| 		t.Fatalf("expected 32, got %d", r.Typ) |  | ||||||
| 	} |  | ||||||
| 	if DebugCap & 1 > 0 { |  | ||||||
| 		if r.Label != "active sym" { |  | ||||||
| 			t.Fatalf("expected 'active sym', got '%s'", r.Label) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,3 +0,0 @@ | |||||||
| url: http://localhost:7123 |  | ||||||
| dial: "*384*96#" |  | ||||||
| phoneNumber: +254722123456 |  | ||||||
| @ -1,21 +0,0 @@ | |||||||
| services: |  | ||||||
|   ussd-pg-store: |  | ||||||
|     image: postgres:17-alpine |  | ||||||
|     restart: unless-stopped |  | ||||||
|     user: postgres |  | ||||||
|     environment: |  | ||||||
|       - POSTGRES_PASSWORD=postgres |  | ||||||
|       - POSTGRES_USER=postgres |  | ||||||
|     volumes: |  | ||||||
|       - ./init_db.sql:/docker-entrypoint-initdb.d/init_db.sql |  | ||||||
|       - ussd-pg:/var/lib/postgresql/data |  | ||||||
|     ports: |  | ||||||
|       - "127.0.0.1:5432:5432" |  | ||||||
|     healthcheck: |  | ||||||
|       test: ["CMD-SHELL", "pg_isready"] |  | ||||||
|       interval: 10s |  | ||||||
|       timeout: 5s |  | ||||||
|       retries: 5 |  | ||||||
| volumes: |  | ||||||
|   ussd-pg: |  | ||||||
|     driver: local |  | ||||||
| @ -1 +0,0 @@ | |||||||
| CREATE DATABASE urdt_ussd; |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| { |  | ||||||
|   "admins": [ |  | ||||||
|     { |  | ||||||
|       "phonenumber" : "<replace with any admin number to test with >" |  | ||||||
|     } |  | ||||||
|    ] |  | ||||||
| } |  | ||||||
| @ -1,47 +0,0 @@ | |||||||
| package commands |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"os" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/utils" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg = logging.NewVanilla().WithDomain("adminstore") |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type Admin struct { |  | ||||||
| 	PhoneNumber string `json:"phonenumber"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Config struct { |  | ||||||
| 	Admins []Admin `json:"admins"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func Seed(ctx context.Context) error { |  | ||||||
| 	var config Config |  | ||||||
| 	adminstore, err := utils.NewAdminStore(ctx, "../admin_numbers") |  | ||||||
| 	store := adminstore.FsStore |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	defer store.Close() |  | ||||||
| 	data, err := os.ReadFile("admin_numbers.json") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	if err := json.Unmarshal(data, &config); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	for _, admin := range config.Admins { |  | ||||||
| 		err := store.Put(ctx, []byte(admin.PhoneNumber), []byte("1")) |  | ||||||
| 		if err != nil { |  | ||||||
| 			logg.Printf(logging.LVL_DEBUG, "Failed to insert admin number", admin.PhoneNumber) |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"log" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/devtools/admin/commands" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func main() { |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	err := commands.Seed(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatalf("Failed to initialize a list of admins with error %s", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| @ -1,126 +0,0 @@ | |||||||
| // create language files from environment
 |  | ||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"flag" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.defalsify.org/vise.git/lang" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/config" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	 |  | ||||||
| 	changeHeadSrc = `LOAD reset_account_authorized 0 |  | ||||||
| LOAD reset_incorrect 0 |  | ||||||
| CATCH incorrect_pin flag_incorrect_pin 1 |  | ||||||
| CATCH pin_entry flag_account_authorized 0 |  | ||||||
| `  |  | ||||||
| 
 |  | ||||||
| 	selectSrc = `LOAD set_language 6 |  | ||||||
| RELOAD set_language |  | ||||||
| CATCH terms flag_account_created 0 |  | ||||||
| MOVE language_changed |  | ||||||
| ` |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg = logging.NewVanilla() |  | ||||||
| 	mouts string |  | ||||||
| 	incmps string |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	initializers.LoadEnvVariables() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func toLanguageLabel(ln lang.Language) string { |  | ||||||
| 	s := ln.Name |  | ||||||
| 	v := strings.Split(s, " (") |  | ||||||
| 	if len(v) > 1 { |  | ||||||
| 		s = v[0] |  | ||||||
| 	} |  | ||||||
| 	return s |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func toLanguageKey(ln lang.Language) string { |  | ||||||
| 	s := toLanguageLabel(ln) |  | ||||||
| 	return strings.ToLower(s) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func main() { |  | ||||||
| 	var srcDir string |  | ||||||
| 
 |  | ||||||
| 	flag.StringVar(&srcDir, "o", ".", "resource dir write to") |  | ||||||
| 	flag.Parse() |  | ||||||
| 
 |  | ||||||
| 	logg.Infof("start command", "dir", srcDir) |  | ||||||
| 
 |  | ||||||
| 	err := config.LoadConfig() |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "config load error: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	logg.Tracef("using languages", "lang", config.Languages) |  | ||||||
| 
 |  | ||||||
| 	for i, v := range(config.Languages) { |  | ||||||
| 		ln, err := lang.LanguageFromCode(v) |  | ||||||
| 		if err != nil { |  | ||||||
| 			fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v) |  | ||||||
| 			os.Exit(1) |  | ||||||
| 		} |  | ||||||
| 		n := i + 1 |  | ||||||
| 		s := toLanguageKey(ln) |  | ||||||
| 		mouts += fmt.Sprintf("MOUT %s %v\n", s, n) |  | ||||||
| 		v = "set_" + ln.Code |  | ||||||
| 		incmps += fmt.Sprintf("INCMP %s %v\n", v, n) |  | ||||||
| 
 |  | ||||||
| 		p := path.Join(srcDir, v) |  | ||||||
| 		w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) |  | ||||||
| 		if err != nil { |  | ||||||
| 			fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err) |  | ||||||
| 			os.Exit(1) |  | ||||||
| 		} |  | ||||||
| 		s = toLanguageLabel(ln) |  | ||||||
| 		defer w.Close() |  | ||||||
| 		_, err = w.Write([]byte(s)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) |  | ||||||
| 			os.Exit(1) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	src := mouts + "HALT\n" + incmps |  | ||||||
| 	src += "INCMP . *\n" |  | ||||||
| 
 |  | ||||||
| 	p := path.Join(srcDir, "select_language.vis") |  | ||||||
| 	w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	defer w.Close() |  | ||||||
| 	_, err = w.Write([]byte(src)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	src = changeHeadSrc + src |  | ||||||
| 	p = path.Join(srcDir, "change_language.vis") |  | ||||||
| 	w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	defer w.Close() |  | ||||||
| 	_, err = w.Write([]byte(src)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,100 +0,0 @@ | |||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"flag" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/config" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/debug" |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg      = logging.NewVanilla() |  | ||||||
| 	scriptDir = path.Join("services", "registration") |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	initializers.LoadEnvVariables() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| func formatItem(k []byte, v []byte) (string, error) { |  | ||||||
| 	o, err := debug.FromKey(k) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
| 	s := fmt.Sprintf("%vValue: %v\n\n", o, string(v)) |  | ||||||
| 	return s, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func main() { |  | ||||||
| 	config.LoadConfig() |  | ||||||
| 
 |  | ||||||
| 	var connStr string |  | ||||||
| 	var sessionId string |  | ||||||
| 	var database string |  | ||||||
| 	var engineDebug bool |  | ||||||
| 	var err error |  | ||||||
| 
 |  | ||||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") |  | ||||||
| 	flag.StringVar(&connStr, "c", ".state", "connection string") |  | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") |  | ||||||
| 	flag.Parse() |  | ||||||
| 
 |  | ||||||
| 	if connStr != "" { |  | ||||||
| 		connStr = config.DbConn |  | ||||||
| 	} |  | ||||||
| 	connData, err := storage.ToConnData(config.DbConn) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	logg.Infof("start command", "conn", connData) |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) |  | ||||||
| 	ctx = context.WithValue(ctx, "Database", database) |  | ||||||
| 
 |  | ||||||
| 	resourceDir := scriptDir |  | ||||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) |  | ||||||
| 
 |  | ||||||
| 	store, err := menuStorageService.GetUserdataDb(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "get userdata db: %v\n", err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	store.SetPrefix(db.DATATYPE_USERDATA) |  | ||||||
| 
 |  | ||||||
| 	d, err := store.Dump(ctx, []byte(sessionId)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "store dump fail: %v\n", err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for true { |  | ||||||
| 		k, v := d.Next(ctx) |  | ||||||
| 		if k == nil { |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 		r, err := formatItem(k, v) |  | ||||||
| 		if err != nil { |  | ||||||
| 			fmt.Fprintf(os.Stderr, "format db item error: %v", err) |  | ||||||
| 			os.Exit(1) |  | ||||||
| 		} |  | ||||||
| 		fmt.Printf(r) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err = store.Close() |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,90 +0,0 @@ | |||||||
| package main |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"crypto/sha1" |  | ||||||
| 	"flag" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/common" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/config" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| 	testdataloader "github.com/peteole/testdata-loader" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg      = logging.NewVanilla() |  | ||||||
| 	baseDir   = testdataloader.GetBasePath() |  | ||||||
| 	scriptDir = path.Join("services", "registration") |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	initializers.LoadEnvVariables() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func main() { |  | ||||||
| 	config.LoadConfig() |  | ||||||
| 
 |  | ||||||
| 	var connStr string |  | ||||||
| 	var sessionId string |  | ||||||
| 	var database string |  | ||||||
| 	var engineDebug bool |  | ||||||
| 	var err error |  | ||||||
| 
 |  | ||||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") |  | ||||||
| 	flag.StringVar(&connStr, "c", "", "connection string") |  | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") |  | ||||||
| 	flag.Parse() |  | ||||||
| 
 |  | ||||||
| 	if connStr != "" { |  | ||||||
| 		connStr = config.DbConn |  | ||||||
| 	} |  | ||||||
| 	connData, err := storage.ToConnData(config.DbConn) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	logg.Infof("start command", "conn", connData) |  | ||||||
| 
 |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) |  | ||||||
| 	ctx = context.WithValue(ctx, "Database", database) |  | ||||||
| 
 |  | ||||||
| 	resourceDir := scriptDir |  | ||||||
| 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) |  | ||||||
| 	 |  | ||||||
| 	store, err := menuStorageService.GetUserdataDb(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 	userStore := common.UserDataStore{store} |  | ||||||
| 
 |  | ||||||
| 	h := sha1.New() |  | ||||||
| 	h.Write([]byte(sessionId)) |  | ||||||
| 	address := h.Sum(nil) |  | ||||||
| 	addressString := fmt.Sprintf("%x", address) |  | ||||||
| 
 |  | ||||||
| 	err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err = store.Close() |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										28
									
								
								doc/data.md
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								doc/data.md
									
									
									
									
									
								
							| @ -1,28 +0,0 @@ | |||||||
| # Internals |  | ||||||
| 
 |  | ||||||
| ## Version |  | ||||||
| 
 |  | ||||||
| This document describes component versions: |  | ||||||
| 
 |  | ||||||
| * `urdt-ussd` `v0.5.0-beta` |  | ||||||
| * `go-vise` `v0.2.2` |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## User profile data |  | ||||||
| 
 |  | ||||||
| All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix. |  | ||||||
| 
 |  | ||||||
| For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key: |  | ||||||
| 
 |  | ||||||
| ``` |  | ||||||
| 0x322b3235343132330002 |  | ||||||
| 
 |  | ||||||
| prefix   sessionid       subprefix |  | ||||||
| 32       2b323534313233  0002 |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### Sub-prefixes |  | ||||||
| 
 |  | ||||||
| All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_` |  | ||||||
| 
 |  | ||||||
| Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item. |  | ||||||
							
								
								
									
										23
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								go.mod
									
									
									
									
									
								
							| @ -1,17 +1,11 @@ | |||||||
| module git.grassecon.net/urdt/ussd | module git.grassecon.net/urdt/ussd | ||||||
| 
 | 
 | ||||||
| go 1.23.0 | go 1.22.6 | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d | 	git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f | ||||||
| 	github.com/alecthomas/assert/v2 v2.2.2 | 	github.com/alecthomas/assert/v2 v2.2.2 | ||||||
| 	github.com/gofrs/uuid v4.4.0+incompatible |  | ||||||
| 	github.com/grassrootseconomics/eth-custodial v1.3.0-beta |  | ||||||
| 	github.com/grassrootseconomics/ussd-data-service v1.2.0-beta |  | ||||||
| 	github.com/joho/godotenv v1.5.1 |  | ||||||
| 	github.com/peteole/testdata-loader v0.3.0 | 	github.com/peteole/testdata-loader v0.3.0 | ||||||
| 	github.com/stretchr/testify v1.9.0 |  | ||||||
| 	golang.org/x/crypto v0.27.0 |  | ||||||
| 	gopkg.in/leonelquinteros/gotext.v1 v1.3.1 | 	gopkg.in/leonelquinteros/gotext.v1 v1.3.1 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -19,21 +13,14 @@ require ( | |||||||
| 	github.com/alecthomas/participle/v2 v2.0.0 // indirect | 	github.com/alecthomas/participle/v2 v2.0.0 // indirect | ||||||
| 	github.com/alecthomas/repr v0.2.0 // indirect | 	github.com/alecthomas/repr v0.2.0 // indirect | ||||||
| 	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect | 	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect | ||||||
| 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/fxamacker/cbor/v2 v2.4.0 // indirect | 	github.com/fxamacker/cbor/v2 v2.4.0 // indirect | ||||||
| 	github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect | 	github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect | ||||||
| 	github.com/hexops/gotextdiff v1.0.3 // indirect | 	github.com/hexops/gotextdiff v1.0.3 // indirect | ||||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect |  | ||||||
| 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect |  | ||||||
| 	github.com/jackc/pgx/v5 v5.7.1 // indirect |  | ||||||
| 	github.com/jackc/puddle/v2 v2.2.2 // indirect |  | ||||||
| 	github.com/kr/text v0.2.0 // indirect |  | ||||||
| 	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect | 	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
| 	github.com/rogpeppe/go-internal v1.13.1 // indirect |  | ||||||
| 	github.com/stretchr/objx v0.5.2 // indirect | 	github.com/stretchr/objx v0.5.2 // indirect | ||||||
|  | 	github.com/stretchr/testify v1.9.0 | ||||||
| 	github.com/x448/float16 v0.8.4 // indirect | 	github.com/x448/float16 v0.8.4 // indirect | ||||||
| 	golang.org/x/sync v0.8.0 // indirect |  | ||||||
| 	golang.org/x/text v0.18.0 // indirect |  | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										50
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								go.sum
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | |||||||
| git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw= | git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f h1:CuJvG3NyMoRtHUim4aZdrfjjJBg2AId7z0yp7Q97bRM= | ||||||
| git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= | git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= | ||||||
| github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= | github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= | ||||||
| github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= | github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= | ||||||
| github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= | github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= | ||||||
| @ -8,67 +8,29 @@ github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk | |||||||
| github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= | github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= | ||||||
| github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE= | github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE= | ||||||
| github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U= | github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U= | ||||||
| github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= |  | ||||||
| github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |  | ||||||
| github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= | github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= | ||||||
| github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= | github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= | ||||||
| github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= |  | ||||||
| github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= |  | ||||||
| github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU= |  | ||||||
| github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= |  | ||||||
| github.com/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk= |  | ||||||
| github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0= |  | ||||||
| github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= | github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= | ||||||
| github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= | github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= | ||||||
| github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= | ||||||
| github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= | ||||||
| github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= |  | ||||||
| github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= |  | ||||||
| github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= |  | ||||||
| github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= |  | ||||||
| github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= |  | ||||||
| github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= |  | ||||||
| github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= |  | ||||||
| github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= |  | ||||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= |  | ||||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= |  | ||||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= |  | ||||||
| github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= |  | ||||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= |  | ||||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= |  | ||||||
| github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= | github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= | ||||||
| github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= | github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= | ||||||
| github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s= |  | ||||||
| github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= |  | ||||||
| github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I= | github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I= | ||||||
| github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U= | github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U= | ||||||
|  | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= |  | ||||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |  | ||||||
| github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= |  | ||||||
| github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= |  | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |  | ||||||
| github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= | ||||||
| github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= | ||||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |  | ||||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |  | ||||||
| github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||||||
| github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||||
| github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= | ||||||
| github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= | ||||||
| golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||||
| golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= |  | ||||||
| golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= |  | ||||||
| golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= |  | ||||||
| golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= |  | ||||||
| golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= |  | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= |  | ||||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= |  | ||||||
| gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= | gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= | ||||||
| gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= | gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= | ||||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |  | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
|  | |||||||
| @ -1,40 +0,0 @@ | |||||||
| package initializers |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"log" |  | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
| 	"strconv" |  | ||||||
| 
 |  | ||||||
| 	"github.com/joho/godotenv" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func LoadEnvVariables() { |  | ||||||
| 	LoadEnvVariablesPath(".") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func LoadEnvVariablesPath(dir string) { |  | ||||||
| 	fp := path.Join(dir, ".env") |  | ||||||
| 	err := godotenv.Load(fp) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatal("Error loading .env file", err) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Helper to get environment variables with a default fallback
 |  | ||||||
| func GetEnv(key, defaultVal string) string { |  | ||||||
| 	if value, exists := os.LookupEnv(key); exists { |  | ||||||
| 		return value |  | ||||||
| 	} |  | ||||||
| 	return defaultVal |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Helper to safely convert environment variables to uint
 |  | ||||||
| func GetEnvUint(key string, defaultVal uint) uint { |  | ||||||
| 	if value, exists := os.LookupEnv(key); exists { |  | ||||||
| 		if parsed, err := strconv.Atoi(value); err == nil && parsed >= 0 { |  | ||||||
| 			return uint(parsed) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return defaultVal |  | ||||||
| } |  | ||||||
| @ -1,34 +0,0 @@ | |||||||
| package args |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/lang" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type LangVar struct { |  | ||||||
| 	v []lang.Language |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(lv *LangVar) Set(s string) error { |  | ||||||
| 	v, err := lang.LanguageFromCode(s) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	lv.v = append(lv.v, v) |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(lv *LangVar) String() string { |  | ||||||
| 	var s []string |  | ||||||
| 	for _, v := range(lv.v) { |  | ||||||
| 		s = append(s, v.Code) |  | ||||||
| 	} |  | ||||||
| 	return strings.Join(s, ",") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(lv *LangVar) Langs() []lang.Language { |  | ||||||
| 	return lv.v |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -6,47 +6,47 @@ import ( | |||||||
| 	"git.defalsify.org/vise.git/persist" | 	"git.defalsify.org/vise.git/persist" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/application" | 	"git.grassecon.net/urdt/ussd/internal/handlers/ussd" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type BaseSessionHandler struct { | type BaseSessionHandler struct { | ||||||
| 	cfgTemplate engine.Config | 	cfgTemplate engine.Config | ||||||
| 	rp          RequestParser | 	rp RequestParser | ||||||
| 	rs          resource.Resource | 	rs resource.Resource | ||||||
| 	hn          *application.Handlers | 	hn *ussd.Handlers | ||||||
| 	provider    storage.StorageProvider | 	provider storage.StorageProvider | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *application.Handlers) *BaseSessionHandler { | func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler { | ||||||
| 	return &BaseSessionHandler{ | 	return &BaseSessionHandler{ | ||||||
| 		cfgTemplate: cfg, | 		cfgTemplate: cfg, | ||||||
| 		rs:          rs, | 		rs: rs, | ||||||
| 		hn:          hn, | 		hn: hn, | ||||||
| 		rp:          rp, | 		rp: rp, | ||||||
| 		provider:    storage.NewSimpleStorageProvider(stateDb, userdataDb), | 		provider: storage.NewSimpleStorageProvider(stateDb, userdataDb), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *BaseSessionHandler) Shutdown() { | func(f* BaseSessionHandler) Shutdown() { | ||||||
| 	err := f.provider.Close() | 	err := f.provider.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.Errorf("handler shutdown error", "err", err) | 		logg.Errorf("handler shutdown error", "err", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine { | func(f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine { | ||||||
| 	en := engine.NewEngine(cfg, rs) | 	en := engine.NewEngine(cfg, rs) | ||||||
| 	en = en.WithPersister(pr) | 	en = en.WithPersister(pr) | ||||||
| 	return en | 	return en | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) { | func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) { | ||||||
| 	var r bool | 	var r bool | ||||||
| 	var err error | 	var err error | ||||||
| 	var ok bool | 	var ok bool | ||||||
| 	 | 	 | ||||||
| 	logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs) | 	logg.InfoCtxf(rqs.Ctx, "new request",  rqs) | ||||||
| 
 | 
 | ||||||
| 	rqs.Storage, err = f.provider.Get(rqs.Config.SessionId) | 	rqs.Storage, err = f.provider.Get(rqs.Config.SessionId) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -55,9 +55,6 @@ func (f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	f.hn = f.hn.WithPersister(rqs.Storage.Persister) | 	f.hn = f.hn.WithPersister(rqs.Storage.Persister) | ||||||
| 	defer func() { |  | ||||||
| 		f.hn.Exit() |  | ||||||
| 	}() |  | ||||||
| 	eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister) | 	eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister) | ||||||
| 	en, ok := eni.(*engine.DefaultEngine) | 	en, ok := eni.(*engine.DefaultEngine) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| @ -74,7 +71,19 @@ func (f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) | |||||||
| 	} | 	} | ||||||
| 	rqs.Engine = en | 	rqs.Engine = en | ||||||
| 
 | 
 | ||||||
| 	r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input) | 	r, err = rqs.Engine.Init(rqs.Ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage) | ||||||
|  | 		rqs.Storage = nil | ||||||
|  | 		if perr != nil { | ||||||
|  | 			logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr) | ||||||
|  | 		} | ||||||
|  | 		return rqs, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if r && len(rqs.Input) > 0 { | ||||||
|  | 		r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input) | ||||||
|  | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage) | 		perr := f.provider.Put(rqs.Config.SessionId, rqs.Storage) | ||||||
| 		rqs.Storage = nil | 		rqs.Storage = nil | ||||||
| @ -88,21 +97,21 @@ func (f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) | |||||||
| 	return rqs, nil | 	return rqs, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession, error) { | func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession,  error) { | ||||||
| 	var err error | 	var err error | ||||||
| 	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) | 	_, err = rqs.Engine.WriteResult(rqs.Ctx, rqs.Writer) | ||||||
| 	return rqs, err | 	return rqs, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) { | func(f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) { | ||||||
| 	defer f.provider.Put(rqs.Config.SessionId, rqs.Storage) | 	defer f.provider.Put(rqs.Config.SessionId, rqs.Storage) | ||||||
| 	return rqs, rqs.Engine.Finish() | 	return rqs, rqs.Engine.Finish() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *BaseSessionHandler) GetConfig() engine.Config { | func(f *BaseSessionHandler) GetConfig() engine.Config { | ||||||
| 	return f.cfgTemplate | 	return f.cfgTemplate | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *BaseSessionHandler) GetRequestParser() RequestParser { | func(f *BaseSessionHandler) GetRequestParser() RequestParser { | ||||||
| 	return f.rp | 	return f.rp | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,141 +0,0 @@ | |||||||
| package handlers |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/asm" |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| 	"git.defalsify.org/vise.git/engine" |  | ||||||
| 	"git.defalsify.org/vise.git/persist" |  | ||||||
| 	"git.defalsify.org/vise.git/resource" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/application" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/utils" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type HandlerService interface { |  | ||||||
| 	GetHandler() (*application.Handlers, error) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func getParser(fp string, debug bool) (*asm.FlagParser, error) { |  | ||||||
| 	flagParser := asm.NewFlagParser().WithDebug() |  | ||||||
| 	_, err := flagParser.Load(fp) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return flagParser, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type LocalHandlerService struct { |  | ||||||
| 	Parser        *asm.FlagParser |  | ||||||
| 	DbRs          *resource.DbResource |  | ||||||
| 	Pe            *persist.Persister |  | ||||||
| 	UserdataStore *db.Db |  | ||||||
| 	AdminStore    *utils.AdminStore |  | ||||||
| 	Cfg           engine.Config |  | ||||||
| 	Rs            resource.Resource |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) { |  | ||||||
| 	parser, err := getParser(fp, debug) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	adminstore, err := utils.NewAdminStore(ctx, "admin_numbers") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return &LocalHandlerService{ |  | ||||||
| 		Parser:     parser, |  | ||||||
| 		DbRs:       dbResource, |  | ||||||
| 		AdminStore: adminstore, |  | ||||||
| 		Cfg:        cfg, |  | ||||||
| 		Rs:         rs, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) { |  | ||||||
| 	ls.Pe = Pe |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ls *LocalHandlerService) SetDataStore(db *db.Db) { |  | ||||||
| 	ls.UserdataStore = db |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*application.Handlers, error) { |  | ||||||
| 	replaceSeparatorFunc := func(input string) string { |  | ||||||
| 		return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	appHandlers, err := application.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	appHandlers = appHandlers.WithPersister(ls.Pe) |  | ||||||
| 	ls.DbRs.AddLocalFunc("set_language", appHandlers.SetLanguage) |  | ||||||
| 	ls.DbRs.AddLocalFunc("create_account", appHandlers.CreateAccount) |  | ||||||
| 	ls.DbRs.AddLocalFunc("save_temporary_pin", appHandlers.SaveTemporaryPin) |  | ||||||
| 	ls.DbRs.AddLocalFunc("verify_create_pin", appHandlers.VerifyCreatePin) |  | ||||||
| 	ls.DbRs.AddLocalFunc("check_identifier", appHandlers.CheckIdentifier) |  | ||||||
| 	ls.DbRs.AddLocalFunc("check_account_status", appHandlers.CheckAccountStatus) |  | ||||||
| 	ls.DbRs.AddLocalFunc("authorize_account", appHandlers.Authorize) |  | ||||||
| 	ls.DbRs.AddLocalFunc("quit", appHandlers.Quit) |  | ||||||
| 	ls.DbRs.AddLocalFunc("check_balance", appHandlers.CheckBalance) |  | ||||||
| 	ls.DbRs.AddLocalFunc("validate_recipient", appHandlers.ValidateRecipient) |  | ||||||
| 	ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset) |  | ||||||
| 	ls.DbRs.AddLocalFunc("invite_valid_recipient", appHandlers.InviteValidRecipient) |  | ||||||
| 	ls.DbRs.AddLocalFunc("max_amount", appHandlers.MaxAmount) |  | ||||||
| 	ls.DbRs.AddLocalFunc("validate_amount", appHandlers.ValidateAmount) |  | ||||||
| 	ls.DbRs.AddLocalFunc("reset_transaction_amount", appHandlers.ResetTransactionAmount) |  | ||||||
| 	ls.DbRs.AddLocalFunc("get_recipient", appHandlers.GetRecipient) |  | ||||||
| 	ls.DbRs.AddLocalFunc("get_sender", appHandlers.GetSender) |  | ||||||
| 	ls.DbRs.AddLocalFunc("get_amount", appHandlers.GetAmount) |  | ||||||
| 	ls.DbRs.AddLocalFunc("reset_incorrect", appHandlers.ResetIncorrectPin) |  | ||||||
| 	ls.DbRs.AddLocalFunc("save_firstname", appHandlers.SaveFirstname) |  | ||||||
| 	ls.DbRs.AddLocalFunc("save_familyname", appHandlers.SaveFamilyname) |  | ||||||
| 	ls.DbRs.AddLocalFunc("save_gender", appHandlers.SaveGender) |  | ||||||
| 	ls.DbRs.AddLocalFunc("save_location", appHandlers.SaveLocation) |  | ||||||
| 	ls.DbRs.AddLocalFunc("save_yob", appHandlers.SaveYob) |  | ||||||
| 	ls.DbRs.AddLocalFunc("save_offerings", appHandlers.SaveOfferings) |  | ||||||
| 	ls.DbRs.AddLocalFunc("reset_account_authorized", appHandlers.ResetAccountAuthorized) |  | ||||||
| 	ls.DbRs.AddLocalFunc("reset_allow_update", appHandlers.ResetAllowUpdate) |  | ||||||
| 	ls.DbRs.AddLocalFunc("get_profile_info", appHandlers.GetProfileInfo) |  | ||||||
| 	ls.DbRs.AddLocalFunc("verify_yob", appHandlers.VerifyYob) |  | ||||||
| 	ls.DbRs.AddLocalFunc("reset_incorrect_date_format", appHandlers.ResetIncorrectYob) |  | ||||||
| 	ls.DbRs.AddLocalFunc("initiate_transaction", appHandlers.InitiateTransaction) |  | ||||||
| 	ls.DbRs.AddLocalFunc("verify_new_pin", appHandlers.VerifyNewPin) |  | ||||||
| 	ls.DbRs.AddLocalFunc("confirm_pin_change", appHandlers.ConfirmPinChange) |  | ||||||
| 	ls.DbRs.AddLocalFunc("quit_with_help", appHandlers.QuitWithHelp) |  | ||||||
| 	ls.DbRs.AddLocalFunc("fetch_community_balance", appHandlers.FetchCommunityBalance) |  | ||||||
| 	ls.DbRs.AddLocalFunc("set_default_voucher", appHandlers.SetDefaultVoucher) |  | ||||||
| 	ls.DbRs.AddLocalFunc("check_vouchers", appHandlers.CheckVouchers) |  | ||||||
| 	ls.DbRs.AddLocalFunc("get_vouchers", appHandlers.GetVoucherList) |  | ||||||
| 	ls.DbRs.AddLocalFunc("view_voucher", appHandlers.ViewVoucher) |  | ||||||
| 	ls.DbRs.AddLocalFunc("set_voucher", appHandlers.SetVoucher) |  | ||||||
| 	ls.DbRs.AddLocalFunc("get_voucher_details", appHandlers.GetVoucherDetails) |  | ||||||
| 	ls.DbRs.AddLocalFunc("reset_valid_pin", appHandlers.ResetValidPin) |  | ||||||
| 	ls.DbRs.AddLocalFunc("check_pin_mismatch", appHandlers.CheckBlockedNumPinMisMatch) |  | ||||||
| 	ls.DbRs.AddLocalFunc("validate_blocked_number", appHandlers.ValidateBlockedNumber) |  | ||||||
| 	ls.DbRs.AddLocalFunc("retrieve_blocked_number", appHandlers.RetrieveBlockedNumber) |  | ||||||
| 	ls.DbRs.AddLocalFunc("reset_unregistered_number", appHandlers.ResetUnregisteredNumber) |  | ||||||
| 	ls.DbRs.AddLocalFunc("reset_others_pin", appHandlers.ResetOthersPin) |  | ||||||
| 	ls.DbRs.AddLocalFunc("save_others_temporary_pin", appHandlers.SaveOthersTemporaryPin) |  | ||||||
| 	ls.DbRs.AddLocalFunc("get_current_profile_info", appHandlers.GetCurrentProfileInfo) |  | ||||||
| 	ls.DbRs.AddLocalFunc("check_transactions", appHandlers.CheckTransactions) |  | ||||||
| 	ls.DbRs.AddLocalFunc("get_transactions", appHandlers.GetTransactionsList) |  | ||||||
| 	ls.DbRs.AddLocalFunc("view_statement", appHandlers.ViewTransactionStatement) |  | ||||||
| 	ls.DbRs.AddLocalFunc("update_all_profile_items", appHandlers.UpdateAllProfileItems) |  | ||||||
| 	ls.DbRs.AddLocalFunc("set_back", appHandlers.SetBack) |  | ||||||
| 	ls.DbRs.AddLocalFunc("show_blocked_account", appHandlers.ShowBlockedAccount) |  | ||||||
| 
 |  | ||||||
| 	return appHandlers, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // TODO: enable setting of sessionId on engine init time
 |  | ||||||
| func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine { |  | ||||||
| 	en := engine.NewEngine(ls.Cfg, ls.Rs) |  | ||||||
| 	en = en.WithPersister(ls.Pe) |  | ||||||
| 	return en |  | ||||||
| } |  | ||||||
							
								
								
									
										112
									
								
								internal/handlers/server/accountservice.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								internal/handlers/server/accountservice.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,112 @@ | |||||||
|  | package server | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"git.grassecon.net/urdt/ussd/config" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/models" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type AccountServiceInterface interface { | ||||||
|  | 	CheckBalance(publicKey string) (string, error) | ||||||
|  | 	CreateAccount() (*models.AccountResponse, error) | ||||||
|  | 	CheckAccountStatus(trackingId string) (string, error) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type AccountService 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
 | ||||||
|  | //     AccountResponse struct can be used here to check the account status during a transaction.
 | ||||||
|  | //
 | ||||||
|  | //
 | ||||||
|  | // 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.
 | ||||||
|  | //
 | ||||||
|  | func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) { | ||||||
|  | 	resp, err := http.Get(config.TrackStatusURL + trackingId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	body, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var trackResp models.TrackStatusResponse | ||||||
|  | 	err = json.Unmarshal(body, &trackResp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	status := trackResp.Result.Transaction.Status | ||||||
|  | 
 | ||||||
|  | 	return status, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | // CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
 | ||||||
|  | // Parameters:
 | ||||||
|  | //   - publicKey: The public key associated with the account whose balance needs to be checked.
 | ||||||
|  | func (as *AccountService) CheckBalance(publicKey string) (string, error) { | ||||||
|  | 
 | ||||||
|  | 	resp, err := http.Get(config.BalanceURL + publicKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "0.0", err | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	body, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "0.0", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var balanceResp models.BalanceResponse | ||||||
|  | 	err = json.Unmarshal(body, &balanceResp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "0.0", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	balance := balanceResp.Result.Balance | ||||||
|  | 	return balance, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | //CreateAccount creates a new account in the custodial system.
 | ||||||
|  | // Returns:
 | ||||||
|  | //   - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
 | ||||||
|  | //     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) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  | 
 | ||||||
|  | 	body, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var accountResp models.AccountResponse | ||||||
|  | 	err = json.Unmarshal(body, &accountResp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &accountResp, nil | ||||||
|  | } | ||||||
| @ -6,9 +6,9 @@ import ( | |||||||
| 	"io" | 	"io" | ||||||
| 
 | 
 | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"git.defalsify.org/vise.git/engine" | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.defalsify.org/vise.git/persist" |  | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
|  | 	"git.defalsify.org/vise.git/persist" | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
| ) | ) | ||||||
| @ -20,26 +20,28 @@ var ( | |||||||
| var ( | var ( | ||||||
| 	ErrInvalidRequest = errors.New("invalid request for context") | 	ErrInvalidRequest = errors.New("invalid request for context") | ||||||
| 	ErrSessionMissing = errors.New("missing session") | 	ErrSessionMissing = errors.New("missing session") | ||||||
| 	ErrInvalidInput   = errors.New("invalid input") | 	ErrInvalidInput = errors.New("invalid input") | ||||||
| 	ErrStorage        = errors.New("storage retrieval fail") | 	ErrStorage = errors.New("storage retrieval fail") | ||||||
| 	ErrEngineType     = errors.New("incompatible engine") | 	ErrEngineType = errors.New("incompatible engine") | ||||||
| 	ErrEngineInit     = errors.New("engine init fail") | 	ErrEngineInit = errors.New("engine init fail") | ||||||
| 	ErrEngineExec     = errors.New("engine exec fail") | 	ErrEngineExec = errors.New("engine exec fail") | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type RequestSession struct { | type RequestSession struct { | ||||||
| 	Ctx      context.Context | 	Ctx context.Context | ||||||
| 	Config   engine.Config | 	Config engine.Config | ||||||
| 	Engine   engine.Engine | 	Engine engine.Engine | ||||||
| 	Input    []byte | 	Input []byte | ||||||
| 	Storage  *storage.Storage | 	Storage *storage.Storage | ||||||
| 	Writer   io.Writer | 	Writer io.Writer | ||||||
| 	Continue bool | 	Continue bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type engineMaker func(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine | ||||||
|  | 
 | ||||||
| // TODO: seems like can remove this.
 | // TODO: seems like can remove this.
 | ||||||
| type RequestParser interface { | type RequestParser interface { | ||||||
| 	GetSessionId(context context.Context, rq any) (string, error) | 	GetSessionId(rq any) (string, error) | ||||||
| 	GetInput(rq any) ([]byte, error) | 	GetInput(rq any) ([]byte, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										1047
									
								
								internal/handlers/ussd/menuhandler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1047
									
								
								internal/handlers/ussd/menuhandler.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1835
									
								
								internal/handlers/ussd/menuhandler_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1835
									
								
								internal/handlers/ussd/menuhandler_test.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										59
									
								
								internal/handlers/ussd/mocks/dbmock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								internal/handlers/ussd/mocks/dbmock.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | package mocks | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/lang" | ||||||
|  | 	"github.com/stretchr/testify/mock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type MockDb struct { | ||||||
|  | 	mock.Mock | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) SetPrefix(prefix uint8) { | ||||||
|  | 	m.Called(prefix) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) Prefix() uint8 { | ||||||
|  | 	args := m.Called() | ||||||
|  | 	return args.Get(0).(uint8) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) Safe() bool { | ||||||
|  | 	args := m.Called() | ||||||
|  | 	return args.Get(0).(bool) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) SetLanguage(language *lang.Language) { | ||||||
|  | 	m.Called(language) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) SetLock(uint8, bool) error { | ||||||
|  | 	args := m.Called() | ||||||
|  | 	return args.Error(0) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) Connect(ctx context.Context, connectionStr string) error { | ||||||
|  | 	args := m.Called(ctx, connectionStr) | ||||||
|  | 	return args.Error(0) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) SetSession(sessionId string) { | ||||||
|  | 	m.Called(sessionId) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) Put(ctx context.Context, key, value []byte) error { | ||||||
|  | 	args := m.Called(ctx, key, value) | ||||||
|  | 	return args.Error(0) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) Get(ctx context.Context, key []byte) ([]byte, error) { | ||||||
|  | 	args := m.Called(ctx, key) | ||||||
|  | 	return nil, args.Error(0) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockDb) Close() error { | ||||||
|  | 	args := m.Called(nil) | ||||||
|  | 	return args.Error(0) | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								internal/handlers/ussd/mocks/servicemock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								internal/handlers/ussd/mocks/servicemock.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | package mocks | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/models" | ||||||
|  | 	"github.com/stretchr/testify/mock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // MockAccountService implements AccountServiceInterface for testing
 | ||||||
|  | type MockAccountService struct { | ||||||
|  | 	mock.Mock | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) { | ||||||
|  | 	args := m.Called() | ||||||
|  | 	return args.Get(0).(*models.AccountResponse), args.Error(1) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockAccountService) CheckBalance(publicKey string) (string, error) { | ||||||
|  | 	args := m.Called(publicKey) | ||||||
|  | 	return args.String(0), args.Error(1) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) { | ||||||
|  | 	args := m.Called(trackingId) | ||||||
|  | 	return args.String(0), args.Error(1) | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								internal/handlers/ussd/mocks/userdbmock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								internal/handlers/ussd/mocks/userdbmock.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | package mocks | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/db" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/utils" | ||||||
|  | 	"github.com/stretchr/testify/mock" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type MockUserDataStore struct { | ||||||
|  | 	db.Db | ||||||
|  | 	mock.Mock | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockUserDataStore) ReadEntry(ctx context.Context, sessionId string, typ utils.DataTyp) ([]byte, error) { | ||||||
|  | 	args := m.Called(ctx, sessionId, typ) | ||||||
|  | 	return args.Get(0).([]byte), args.Error(1) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *MockUserDataStore) WriteEntry(ctx context.Context, sessionId string, typ utils.DataTyp, value []byte) error { | ||||||
|  | 	args := m.Called(ctx, sessionId, typ, value) | ||||||
|  | 	return args.Error(0) | ||||||
|  | } | ||||||
| @ -1,120 +0,0 @@ | |||||||
| package at |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/url" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/common" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type ATRequestParser struct { |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (arp *ATRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) { |  | ||||||
| 	rqv, ok := rq.(*http.Request) |  | ||||||
| 	if !ok { |  | ||||||
| 		logg.Warnf("got an invalid request", "req", rq) |  | ||||||
| 		return "", handlers.ErrInvalidRequest |  | ||||||
| 	} |  | ||||||
| 	// Capture body (if any) for logging
 |  | ||||||
| 	body, err := io.ReadAll(rqv.Body) |  | ||||||
| 	if err != nil { |  | ||||||
| 		logg.Warnf("failed to read request body", "err", err) |  | ||||||
| 		return "", fmt.Errorf("failed to read request body: %v", err) |  | ||||||
| 	} |  | ||||||
| 	// Reset the body for further reading
 |  | ||||||
| 	rqv.Body = io.NopCloser(bytes.NewReader(body)) |  | ||||||
| 
 |  | ||||||
| 	// Log the body as JSON
 |  | ||||||
| 	bodyLog := map[string]string{"body": string(body)} |  | ||||||
| 	logBytes, err := json.Marshal(bodyLog) |  | ||||||
| 	if err != nil { |  | ||||||
| 		logg.Warnf("failed to marshal request body", "err", err) |  | ||||||
| 	} else { |  | ||||||
| 		decodedStr := string(logBytes) |  | ||||||
| 		sessionId, err := extractATSessionId(decodedStr) |  | ||||||
| 		if err != nil { |  | ||||||
| 			ctx = context.WithValue(ctx, "AT-SessionId", sessionId) |  | ||||||
| 		} |  | ||||||
| 		logg.DebugCtxf(ctx, "Received request:", decodedStr) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err := rqv.ParseForm(); err != nil { |  | ||||||
| 		logg.Warnf("failed to parse form data", "err", err) |  | ||||||
| 		return "", fmt.Errorf("failed to parse form data: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	phoneNumber := rqv.FormValue("phoneNumber") |  | ||||||
| 	if phoneNumber == "" { |  | ||||||
| 		return "", fmt.Errorf("no phone number found") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	formattedNumber, err := common.FormatPhoneNumber(phoneNumber) |  | ||||||
| 	if err != nil { |  | ||||||
| 		logg.Warnf("failed to format phone number", "err", err) |  | ||||||
| 		return "", fmt.Errorf("failed to format number") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return formattedNumber, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (arp *ATRequestParser) GetInput(rq any) ([]byte, error) { |  | ||||||
| 	rqv, ok := rq.(*http.Request) |  | ||||||
| 	if !ok { |  | ||||||
| 		return nil, handlers.ErrInvalidRequest |  | ||||||
| 	} |  | ||||||
| 	if err := rqv.ParseForm(); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to parse form data: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	text := rqv.FormValue("text") |  | ||||||
| 
 |  | ||||||
| 	parts := strings.Split(text, "*") |  | ||||||
| 	if len(parts) == 0 { |  | ||||||
| 		return nil, fmt.Errorf("no input found") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	trimmedInput := strings.TrimSpace(parts[len(parts)-1]) |  | ||||||
| 	return []byte(trimmedInput), nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func parseQueryParams(query string) map[string]string { |  | ||||||
| 	params := make(map[string]string) |  | ||||||
| 
 |  | ||||||
| 	queryParams := strings.Split(query, "&") |  | ||||||
| 	for _, param := range queryParams { |  | ||||||
| 		// Split each key-value pair by '='
 |  | ||||||
| 		parts := strings.SplitN(param, "=", 2) |  | ||||||
| 		if len(parts) == 2 { |  | ||||||
| 			params[parts[0]] = parts[1] |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return params |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func extractATSessionId(decodedStr string) (string, error) { |  | ||||||
| 	var data map[string]string |  | ||||||
| 	err := json.Unmarshal([]byte(decodedStr), &data) |  | ||||||
| 
 |  | ||||||
| 	if err != nil { |  | ||||||
| 		logg.Errorf("Error unmarshalling JSON: %v", err) |  | ||||||
| 		return "", nil |  | ||||||
| 	} |  | ||||||
| 	decodedBody, err := url.QueryUnescape(data["body"]) |  | ||||||
| 	if err != nil { |  | ||||||
| 		logg.Errorf("Error URL-decoding body: %v", err) |  | ||||||
| 		return "", nil |  | ||||||
| 	} |  | ||||||
| 	params := parseQueryParams(decodedBody) |  | ||||||
| 
 |  | ||||||
| 	sessionId := params["sessionId"] |  | ||||||
| 	return sessionId, nil |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| @ -1,234 +0,0 @@ | |||||||
| package at |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"net/url" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/engine" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestNewATSessionHandler(t *testing.T) { |  | ||||||
| 	mockHandler := &httpmocks.MockRequestHandler{} |  | ||||||
| 	ash := NewATSessionHandler(mockHandler) |  | ||||||
| 
 |  | ||||||
| 	if ash == nil { |  | ||||||
| 		t.Fatal("NewATSessionHandler returned nil") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if ash.SessionHandler == nil { |  | ||||||
| 		t.Fatal("SessionHandler is nil") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestATSessionHandler_ServeHTTP(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name           string |  | ||||||
| 		setupMocks     func(*httpmocks.MockRequestHandler, *httpmocks.MockRequestParser, *httpmocks.MockEngine) |  | ||||||
| 		formData       url.Values |  | ||||||
| 		expectedStatus int |  | ||||||
| 		expectedBody   string |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name: "Successful request", |  | ||||||
| 			setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) { |  | ||||||
| 				mrp.GetSessionIdFunc = func(rq any) (string, error) { |  | ||||||
| 					req := rq.(*http.Request) |  | ||||||
| 					return req.FormValue("phoneNumber"), nil |  | ||||||
| 				} |  | ||||||
| 				mrp.GetInputFunc = func(rq any) ([]byte, error) { |  | ||||||
| 					req := rq.(*http.Request) |  | ||||||
| 					text := req.FormValue("text") |  | ||||||
| 					parts := strings.Split(text, "*") |  | ||||||
| 					return []byte(parts[len(parts)-1]), nil |  | ||||||
| 				} |  | ||||||
| 				mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 					rqs.Continue = true |  | ||||||
| 					rqs.Engine = me |  | ||||||
| 					return rqs, nil |  | ||||||
| 				} |  | ||||||
| 				mh.GetConfigFunc = func() engine.Config { return engine.Config{} } |  | ||||||
| 				mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp } |  | ||||||
| 				mh.OutputFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil } |  | ||||||
| 				mh.ResetFunc = func(rs handlers.RequestSession) (handlers.RequestSession, error) { return rs, nil } |  | ||||||
| 				me.FlushFunc = func(context.Context, io.Writer) (int, error) { return 0, nil } |  | ||||||
| 			}, |  | ||||||
| 			formData: url.Values{ |  | ||||||
| 				"phoneNumber": []string{"+1234567890"}, |  | ||||||
| 				"text":        []string{"1*2*3"}, |  | ||||||
| 			}, |  | ||||||
| 			expectedStatus: http.StatusOK, |  | ||||||
| 			expectedBody:   "CON ", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "GetSessionId error", |  | ||||||
| 			setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) { |  | ||||||
| 				mrp.GetSessionIdFunc = func(rq any) (string, error) { |  | ||||||
| 					return "", errors.New("no phone number found") |  | ||||||
| 				} |  | ||||||
| 				mh.GetConfigFunc = func() engine.Config { return engine.Config{} } |  | ||||||
| 				mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp } |  | ||||||
| 			}, |  | ||||||
| 			formData: url.Values{ |  | ||||||
| 				"text": []string{"1*2*3"}, |  | ||||||
| 			}, |  | ||||||
| 			expectedStatus: http.StatusBadRequest, |  | ||||||
| 			expectedBody:   "", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "GetInput error", |  | ||||||
| 			setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) { |  | ||||||
| 				mrp.GetSessionIdFunc = func(rq any) (string, error) { |  | ||||||
| 					req := rq.(*http.Request) |  | ||||||
| 					return req.FormValue("phoneNumber"), nil |  | ||||||
| 				} |  | ||||||
| 				mrp.GetInputFunc = func(rq any) ([]byte, error) { |  | ||||||
| 					return nil, errors.New("no input found") |  | ||||||
| 				} |  | ||||||
| 				mh.GetConfigFunc = func() engine.Config { return engine.Config{} } |  | ||||||
| 				mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp } |  | ||||||
| 			}, |  | ||||||
| 			formData: url.Values{ |  | ||||||
| 				"phoneNumber": []string{"+1234567890"}, |  | ||||||
| 			}, |  | ||||||
| 			expectedStatus: http.StatusBadRequest, |  | ||||||
| 			expectedBody:   "", |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Process error", |  | ||||||
| 			setupMocks: func(mh *httpmocks.MockRequestHandler, mrp *httpmocks.MockRequestParser, me *httpmocks.MockEngine) { |  | ||||||
| 				mrp.GetSessionIdFunc = func(rq any) (string, error) { |  | ||||||
| 					req := rq.(*http.Request) |  | ||||||
| 					return req.FormValue("phoneNumber"), nil |  | ||||||
| 				} |  | ||||||
| 				mrp.GetInputFunc = func(rq any) ([]byte, error) { |  | ||||||
| 					req := rq.(*http.Request) |  | ||||||
| 					text := req.FormValue("text") |  | ||||||
| 					parts := strings.Split(text, "*") |  | ||||||
| 					return []byte(parts[len(parts)-1]), nil |  | ||||||
| 				} |  | ||||||
| 				mh.ProcessFunc = func(rqs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 					return rqs, handlers.ErrStorage |  | ||||||
| 				} |  | ||||||
| 				mh.GetConfigFunc = func() engine.Config { return engine.Config{} } |  | ||||||
| 				mh.GetRequestParserFunc = func() handlers.RequestParser { return mrp } |  | ||||||
| 			}, |  | ||||||
| 			formData: url.Values{ |  | ||||||
| 				"phoneNumber": []string{"+1234567890"}, |  | ||||||
| 				"text":        []string{"1*2*3"}, |  | ||||||
| 			}, |  | ||||||
| 			expectedStatus: http.StatusInternalServerError, |  | ||||||
| 			expectedBody:   "", |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			mockHandler := &httpmocks.MockRequestHandler{} |  | ||||||
| 			mockRequestParser := &httpmocks.MockRequestParser{} |  | ||||||
| 			mockEngine := &httpmocks.MockEngine{} |  | ||||||
| 			tt.setupMocks(mockHandler, mockRequestParser, mockEngine) |  | ||||||
| 
 |  | ||||||
| 			ash := NewATSessionHandler(mockHandler) |  | ||||||
| 
 |  | ||||||
| 			req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tt.formData.Encode())) |  | ||||||
| 			req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |  | ||||||
| 			w := httptest.NewRecorder() |  | ||||||
| 
 |  | ||||||
| 			ash.ServeHTTP(w, req) |  | ||||||
| 
 |  | ||||||
| 			if w.Code != tt.expectedStatus { |  | ||||||
| 				t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if tt.expectedBody != "" && w.Body.String() != tt.expectedBody { |  | ||||||
| 				t.Errorf("Expected body %q, got %q", tt.expectedBody, w.Body.String()) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestATSessionHandler_Output(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name           string |  | ||||||
| 		input          handlers.RequestSession |  | ||||||
| 		expectedPrefix string |  | ||||||
| 		expectedError  bool |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name: "Continue true", |  | ||||||
| 			input: handlers.RequestSession{ |  | ||||||
| 				Continue: true, |  | ||||||
| 				Engine: &httpmocks.MockEngine{ |  | ||||||
| 					FlushFunc: func(context.Context, io.Writer) (int, error) { |  | ||||||
| 						return 0, nil |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 				Writer: &httpmocks.MockWriter{}, |  | ||||||
| 			}, |  | ||||||
| 			expectedPrefix: "CON ", |  | ||||||
| 			expectedError:  false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Continue false", |  | ||||||
| 			input: handlers.RequestSession{ |  | ||||||
| 				Continue: false, |  | ||||||
| 				Engine: &httpmocks.MockEngine{ |  | ||||||
| 					FlushFunc: func(context.Context, io.Writer) (int, error) { |  | ||||||
| 						return 0, nil |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 				Writer: &httpmocks.MockWriter{}, |  | ||||||
| 			}, |  | ||||||
| 			expectedPrefix: "END ", |  | ||||||
| 			expectedError:  false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Flush error", |  | ||||||
| 			input: handlers.RequestSession{ |  | ||||||
| 				Continue: true, |  | ||||||
| 				Engine: &httpmocks.MockEngine{ |  | ||||||
| 					FlushFunc: func(context.Context, io.Writer) (int, error) { |  | ||||||
| 						return 0, errors.New("write error") |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 				Writer: &httpmocks.MockWriter{}, |  | ||||||
| 			}, |  | ||||||
| 			expectedPrefix: "CON ", |  | ||||||
| 			expectedError:  true, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			ash := &ATSessionHandler{} |  | ||||||
| 			_, err := ash.Output(tt.input) |  | ||||||
| 
 |  | ||||||
| 			if tt.expectedError && err == nil { |  | ||||||
| 				t.Error("Expected an error, but got nil") |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if !tt.expectedError && err != nil { |  | ||||||
| 				t.Errorf("Unexpected error: %v", err) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			mw := tt.input.Writer.(*httpmocks.MockWriter) |  | ||||||
| 			if !mw.WriteStringCalled { |  | ||||||
| 				t.Error("WriteString was not called") |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if mw.WrittenString != tt.expectedPrefix { |  | ||||||
| 				t.Errorf("Expected prefix %q, got %q", tt.expectedPrefix, mw.WrittenString) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @ -1,25 +1,19 @@ | |||||||
| package at | package http | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg = logging.NewVanilla().WithDomain("atserver").WithContextKey("SessionId").WithContextKey("AT-SessionId") |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type ATSessionHandler struct { | type ATSessionHandler struct { | ||||||
| 	*httpserver.SessionHandler | 	*SessionHandler | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler { | func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler { | ||||||
| 	return &ATSessionHandler{ | 	return &ATSessionHandler{ | ||||||
| 		SessionHandler: httpserver.ToSessionHandler(h), | 		SessionHandler: ToSessionHandler(h), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -34,32 +28,33 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) | |||||||
| 
 | 
 | ||||||
| 	rp := ash.GetRequestParser() | 	rp := ash.GetRequestParser() | ||||||
| 	cfg := ash.GetConfig() | 	cfg := ash.GetConfig() | ||||||
| 	cfg.SessionId, err = rp.GetSessionId(req.Context(), req) | 	cfg.SessionId, err = rp.GetSessionId(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||||
| 		ash.WriteError(w, 400, err) | 		ash.writeError(w, 400, err) | ||||||
| 		return |  | ||||||
| 	} | 	} | ||||||
| 	rqs.Config = cfg | 	rqs.Config = cfg | ||||||
| 	rqs.Input, err = rp.GetInput(req) | 	rqs.Input, err = rp.GetInput(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||||
| 		ash.WriteError(w, 400, err) | 		ash.writeError(w, 400, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	rqs, err = ash.Process(rqs) | 	rqs, err = ash.Process(rqs) | ||||||
| 	switch err { | 	switch err { | ||||||
| 	case nil: // set code to 200 if no err
 | 	case handlers.ErrStorage: | ||||||
| 		code = 200 | 		code = 500 | ||||||
| 	case handlers.ErrStorage, handlers.ErrEngineInit, handlers.ErrEngineExec, handlers.ErrEngineType: | 	case handlers.ErrEngineInit: | ||||||
|  | 		code = 500 | ||||||
|  | 	case handlers.ErrEngineExec: | ||||||
| 		code = 500 | 		code = 500 | ||||||
| 	default: | 	default: | ||||||
| 		code = 500 | 		code = 200 | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if code != 200 { | 	if code != 200 { | ||||||
| 		ash.WriteError(w, 500, err) | 		ash.writeError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -67,13 +62,13 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) | |||||||
| 	w.Header().Set("Content-Type", "text/plain") | 	w.Header().Set("Content-Type", "text/plain") | ||||||
| 	rqs, err = ash.Output(rqs) | 	rqs, err = ash.Output(rqs) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ash.WriteError(w, 500, err) | 		ash.writeError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	rqs, err = ash.Reset(rqs) | 	rqs, err = ash.Reset(rqs) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ash.WriteError(w, 500, err) | 		ash.writeError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @ -93,6 +88,6 @@ func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.Reque | |||||||
| 		return rqs, err | 		return rqs, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) | 	_, err = rqs.Engine.WriteResult(rqs.Ctx, rqs.Writer) | ||||||
| 	return rqs, err | 	return rqs, err | ||||||
| } | } | ||||||
| @ -1,37 +0,0 @@ | |||||||
| package http |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type DefaultRequestParser struct { |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) { |  | ||||||
| 	rqv, ok := rq.(*http.Request) |  | ||||||
| 	if !ok { |  | ||||||
| 		return "", handlers.ErrInvalidRequest |  | ||||||
| 	} |  | ||||||
| 	v := rqv.Header.Get("X-Vise-Session") |  | ||||||
| 	if v == "" { |  | ||||||
| 		return "", handlers.ErrSessionMissing |  | ||||||
| 	} |  | ||||||
| 	return v, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) { |  | ||||||
| 	rqv, ok := rq.(*http.Request) |  | ||||||
| 	if !ok { |  | ||||||
| 		return nil, handlers.ErrInvalidRequest |  | ||||||
| 	} |  | ||||||
| 	defer rqv.Body.Close() |  | ||||||
| 	v, err := ioutil.ReadAll(rqv.Body) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return v, nil |  | ||||||
| } |  | ||||||
| @ -1,6 +1,7 @@ | |||||||
| package http | package http | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| @ -13,6 +14,35 @@ var ( | |||||||
| 	logg = logging.NewVanilla().WithDomain("httpserver") | 	logg = logging.NewVanilla().WithDomain("httpserver") | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | type DefaultRequestParser struct { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) { | ||||||
|  | 	rqv, ok := rq.(*http.Request) | ||||||
|  | 	if !ok { | ||||||
|  | 		return "", handlers.ErrInvalidRequest | ||||||
|  | 	} | ||||||
|  | 	v := rqv.Header.Get("X-Vise-Session") | ||||||
|  | 	if v == "" { | ||||||
|  | 		return "", handlers.ErrSessionMissing | ||||||
|  | 	} | ||||||
|  | 	return v, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) { | ||||||
|  | 	rqv, ok := rq.(*http.Request) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, handlers.ErrInvalidRequest | ||||||
|  | 	} | ||||||
|  | 	defer rqv.Body.Close() | ||||||
|  | 	v, err := ioutil.ReadAll(rqv.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return v, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type SessionHandler struct { | type SessionHandler struct { | ||||||
| 	handlers.RequestHandler | 	handlers.RequestHandler | ||||||
| } | } | ||||||
| @ -23,39 +53,40 @@ func ToSessionHandler(h handlers.RequestHandler) *SessionHandler { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *SessionHandler) WriteError(w http.ResponseWriter, code int, err error) { | func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) { | ||||||
| 	s := err.Error() | 	s := err.Error() | ||||||
| 	w.Header().Set("Content-Length", strconv.Itoa(len(s))) | 	w.Header().Set("Content-Length", strconv.Itoa(len(s))) | ||||||
| 	w.WriteHeader(code) | 	w.WriteHeader(code) | ||||||
| 	_, err = w.Write([]byte(s)) | 	_, err = w.Write([]byte{}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.Errorf("error writing error!!", "err", err, "olderr", s) | 		logg.Errorf("error writing error!!", "err", err, "olderr", s) | ||||||
| 		w.WriteHeader(500) | 		w.WriteHeader(500) | ||||||
| 	} | 	} | ||||||
|  | 	return  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||||
| 	var code int | 	var code int | ||||||
| 	var err error | 	var err error | ||||||
| 	var perr error | 	var perr error | ||||||
| 
 | 
 | ||||||
| 	rqs := handlers.RequestSession{ | 	rqs := handlers.RequestSession{ | ||||||
| 		Ctx:    req.Context(), | 		Ctx: req.Context(), | ||||||
| 		Writer: w, | 		Writer: w, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	rp := f.GetRequestParser() | 	rp := f.GetRequestParser() | ||||||
| 	cfg := f.GetConfig() | 	cfg := f.GetConfig() | ||||||
| 	cfg.SessionId, err = rp.GetSessionId(req.Context(), req) | 	cfg.SessionId, err = rp.GetSessionId(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||||
| 		f.WriteError(w, 400, err) | 		f.writeError(w, 400, err) | ||||||
| 	} | 	} | ||||||
| 	rqs.Config = cfg | 	rqs.Config = cfg | ||||||
| 	rqs.Input, err = rp.GetInput(req) | 	rqs.Input, err = rp.GetInput(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||||
| 		f.WriteError(w, 400, err) | 		f.writeError(w, 400, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -72,7 +103,7 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if code != 200 { | 	if code != 200 { | ||||||
| 		f.WriteError(w, 500, err) | 		f.writeError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -81,11 +112,11 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |||||||
| 	rqs, err = f.Output(rqs) | 	rqs, err = f.Output(rqs) | ||||||
| 	rqs, perr = f.Reset(rqs) | 	rqs, perr = f.Reset(rqs) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		f.WriteError(w, 500, err) | 		f.writeError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if perr != nil { | 	if perr != nil { | ||||||
| 		f.WriteError(w, 500, perr) | 		f.writeError(w, 500, perr) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,230 +0,0 @@ | |||||||
| package http |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/engine" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // invalidRequestType is a custom type to test invalid request scenarios
 |  | ||||||
| type invalidRequestType struct{} |  | ||||||
| 
 |  | ||||||
| // errorReader is a helper type that always returns an error when Read is called
 |  | ||||||
| type errorReader struct{} |  | ||||||
| 
 |  | ||||||
| func (e *errorReader) Read(p []byte) (n int, err error) { |  | ||||||
| 	return 0, errors.New("read error") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestSessionHandler_ServeHTTP(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name           string |  | ||||||
| 		sessionID      string |  | ||||||
| 		input          []byte |  | ||||||
| 		parserErr      error |  | ||||||
| 		processErr     error |  | ||||||
| 		outputErr      error |  | ||||||
| 		resetErr       error |  | ||||||
| 		expectedStatus int |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name:           "Success", |  | ||||||
| 			sessionID:      "123", |  | ||||||
| 			input:          []byte("test input"), |  | ||||||
| 			expectedStatus: http.StatusOK, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:           "Missing Session ID", |  | ||||||
| 			sessionID:      "", |  | ||||||
| 			parserErr:      handlers.ErrSessionMissing, |  | ||||||
| 			expectedStatus: http.StatusBadRequest, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:           "Process Error", |  | ||||||
| 			sessionID:      "123", |  | ||||||
| 			input:          []byte("test input"), |  | ||||||
| 			processErr:     handlers.ErrStorage, |  | ||||||
| 			expectedStatus: http.StatusInternalServerError, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:           "Output Error", |  | ||||||
| 			sessionID:      "123", |  | ||||||
| 			input:          []byte("test input"), |  | ||||||
| 			outputErr:      errors.New("output error"), |  | ||||||
| 			expectedStatus: http.StatusOK, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:           "Reset Error", |  | ||||||
| 			sessionID:      "123", |  | ||||||
| 			input:          []byte("test input"), |  | ||||||
| 			resetErr:       errors.New("reset error"), |  | ||||||
| 			expectedStatus: http.StatusOK, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			mockRequestParser := &httpmocks.MockRequestParser{ |  | ||||||
| 				GetSessionIdFunc: func(any) (string, error) { |  | ||||||
| 					return tt.sessionID, tt.parserErr |  | ||||||
| 				}, |  | ||||||
| 				GetInputFunc: func(any) ([]byte, error) { |  | ||||||
| 					return tt.input, nil |  | ||||||
| 				}, |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			mockRequestHandler := &httpmocks.MockRequestHandler{ |  | ||||||
| 				ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 					return rs, tt.processErr |  | ||||||
| 				}, |  | ||||||
| 				OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 					return rs, tt.outputErr |  | ||||||
| 				}, |  | ||||||
| 				ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 					return rs, tt.resetErr |  | ||||||
| 				}, |  | ||||||
| 				GetRequestParserFunc: func() handlers.RequestParser { |  | ||||||
| 					return mockRequestParser |  | ||||||
| 				}, |  | ||||||
| 				GetConfigFunc: func() engine.Config { |  | ||||||
| 					return engine.Config{} |  | ||||||
| 				}, |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			sessionHandler := ToSessionHandler(mockRequestHandler) |  | ||||||
| 
 |  | ||||||
| 			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input)) |  | ||||||
| 			req.Header.Set("X-Vise-Session", tt.sessionID) |  | ||||||
| 
 |  | ||||||
| 			rr := httptest.NewRecorder() |  | ||||||
| 
 |  | ||||||
| 			sessionHandler.ServeHTTP(rr, req) |  | ||||||
| 
 |  | ||||||
| 			if status := rr.Code; status != tt.expectedStatus { |  | ||||||
| 				t.Errorf("handler returned wrong status code: got %v want %v", |  | ||||||
| 					status, tt.expectedStatus) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestSessionHandler_WriteError(t *testing.T) { |  | ||||||
| 	handler := &SessionHandler{} |  | ||||||
| 	mockWriter := &httpmocks.MockWriter{} |  | ||||||
| 	err := errors.New("test error") |  | ||||||
| 
 |  | ||||||
| 	handler.WriteError(mockWriter, http.StatusBadRequest, err) |  | ||||||
| 
 |  | ||||||
| 	if mockWriter.WrittenString != "" { |  | ||||||
| 		t.Errorf("Expected empty body, got %s", mockWriter.WrittenString) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDefaultRequestParser_GetSessionId(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name          string |  | ||||||
| 		request       any |  | ||||||
| 		expectedID    string |  | ||||||
| 		expectedError error |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name: "Valid Session ID", |  | ||||||
| 			request: func() *http.Request { |  | ||||||
| 				req := httptest.NewRequest(http.MethodPost, "/", nil) |  | ||||||
| 				req.Header.Set("X-Vise-Session", "123456") |  | ||||||
| 				return req |  | ||||||
| 			}(), |  | ||||||
| 			expectedID:    "123456", |  | ||||||
| 			expectedError: nil, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:          "Missing Session ID", |  | ||||||
| 			request:       httptest.NewRequest(http.MethodPost, "/", nil), |  | ||||||
| 			expectedID:    "", |  | ||||||
| 			expectedError: handlers.ErrSessionMissing, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:          "Invalid Request Type", |  | ||||||
| 			request:       invalidRequestType{}, |  | ||||||
| 			expectedID:    "", |  | ||||||
| 			expectedError: handlers.ErrInvalidRequest, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	parser := &DefaultRequestParser{} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			id, err := parser.GetSessionId(context.Background(),tt.request) |  | ||||||
| 
 |  | ||||||
| 			if id != tt.expectedID { |  | ||||||
| 				t.Errorf("Expected session ID %s, got %s", tt.expectedID, id) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if err != tt.expectedError { |  | ||||||
| 				t.Errorf("Expected error %v, got %v", tt.expectedError, err) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDefaultRequestParser_GetInput(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name          string |  | ||||||
| 		request       any |  | ||||||
| 		expectedInput []byte |  | ||||||
| 		expectedError error |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name: "Valid Input", |  | ||||||
| 			request: func() *http.Request { |  | ||||||
| 				return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input")) |  | ||||||
| 			}(), |  | ||||||
| 			expectedInput: []byte("test input"), |  | ||||||
| 			expectedError: nil, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:          "Empty Input", |  | ||||||
| 			request:       httptest.NewRequest(http.MethodPost, "/", nil), |  | ||||||
| 			expectedInput: []byte{}, |  | ||||||
| 			expectedError: nil, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:          "Invalid Request Type", |  | ||||||
| 			request:       invalidRequestType{}, |  | ||||||
| 			expectedInput: nil, |  | ||||||
| 			expectedError: handlers.ErrInvalidRequest, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Read Error", |  | ||||||
| 			request: func() *http.Request { |  | ||||||
| 				return httptest.NewRequest(http.MethodPost, "/", &errorReader{}) |  | ||||||
| 			}(), |  | ||||||
| 			expectedInput: nil, |  | ||||||
| 			expectedError: errors.New("read error"), |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	parser := &DefaultRequestParser{} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			input, err := parser.GetInput(tt.request) |  | ||||||
| 
 |  | ||||||
| 			if !bytes.Equal(input, tt.expectedInput) { |  | ||||||
| 				t.Errorf("Expected input %s, got %s", tt.expectedInput, input) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) { |  | ||||||
| 				t.Errorf("Expected error %v, got %v", tt.expectedError, err) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										15
									
								
								internal/models/accountresponse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/models/accountresponse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | 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"` | ||||||
|  | 	} `json:"result"` | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								internal/models/balanceresponse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/models/balanceresponse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | package models | ||||||
|  | 
 | ||||||
|  | import "encoding/json" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | type BalanceResponse struct { | ||||||
|  | 	Ok     bool `json:"ok"` | ||||||
|  | 	Result struct { | ||||||
|  | 		Balance string      `json:"balance"` | ||||||
|  | 		Nonce   json.Number `json:"nonce"` | ||||||
|  | 	} `json:"result"` | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								internal/models/trackstatusresponse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								internal/models/trackstatusresponse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | package models | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | type TrackStatusResponse struct { | ||||||
|  | 	Ok     bool `json:"ok"` | ||||||
|  | 	Result struct { | ||||||
|  | 		Transaction struct { | ||||||
|  | 			CreatedAt     time.Time   `json:"createdAt"` | ||||||
|  | 			Status        string      `json:"status"` | ||||||
|  | 			TransferValue json.Number `json:"transferValue"` | ||||||
|  | 			TxHash        string      `json:"txHash"` | ||||||
|  | 			TxType        string      `json:"txType"` | ||||||
|  | 		} | ||||||
|  | 	} `json:"result"` | ||||||
|  | } | ||||||
| @ -1,65 +0,0 @@ | |||||||
| package ssh |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
| 
 |  | ||||||
| 	"golang.org/x/crypto/ssh" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| 	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type SshKeyStore struct { |  | ||||||
| 	store db.Db |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewSshKeyStore(ctx context.Context, dbDir string) (*SshKeyStore, error) { |  | ||||||
| 	keyStore := &SshKeyStore{} |  | ||||||
| 	keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm") |  | ||||||
| 	keyStore.store = dbstorage.NewThreadGdbmDb() |  | ||||||
| 	err := keyStore.store.Connect(ctx, keyStoreFile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return keyStore, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(s *SshKeyStore) AddFromFile(ctx context.Context, fp string, sessionId string) error { |  | ||||||
| 	_, err := os.Stat(fp) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("cannot open ssh server public key file: %v\n", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	publicBytes, err := os.ReadFile(fp) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("Failed to load public key: %v", err) |  | ||||||
| 	} |  | ||||||
| 	pubKey, _, _, _, err := ssh.ParseAuthorizedKey(publicBytes) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("Failed to parse public key: %v", err) |  | ||||||
| 	} |  | ||||||
| 	k := append([]byte{0x01}, pubKey.Marshal()...) |  | ||||||
| 	s.store.SetPrefix(storage.DATATYPE_EXTEND) |  | ||||||
| 	logg.Infof("Added key", "sessionId", sessionId, "public key", string(publicBytes)) |  | ||||||
| 	return s.store.Put(ctx, k, []byte(sessionId)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(s *SshKeyStore) Get(ctx context.Context, pubKey ssh.PublicKey) (string, error) { |  | ||||||
| 	s.store.SetLanguage(nil) |  | ||||||
| 	s.store.SetPrefix(storage.DATATYPE_EXTEND) |  | ||||||
| 	k := append([]byte{0x01}, pubKey.Marshal()...) |  | ||||||
| 	v, err := s.store.Get(ctx, k) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
| 	return string(v), nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(s *SshKeyStore) Close() error { |  | ||||||
| 	return s.store.Close() |  | ||||||
| } |  | ||||||
| @ -1,284 +0,0 @@ | |||||||
| package ssh |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/hex" |  | ||||||
| 	"encoding/base64" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"net" |  | ||||||
| 	"os" |  | ||||||
| 	"sync" |  | ||||||
| 
 |  | ||||||
| 	"golang.org/x/crypto/ssh" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/engine" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.defalsify.org/vise.git/resource" |  | ||||||
| 	"git.defalsify.org/vise.git/state" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg = logging.NewVanilla().WithDomain("ssh") |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type auther struct { |  | ||||||
| 	Ctx context.Context |  | ||||||
| 	keyStore *SshKeyStore |  | ||||||
| 	auth map[string]string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther { |  | ||||||
| 	return &auther{ |  | ||||||
| 		Ctx: ctx, |  | ||||||
| 		keyStore: keyStore, |  | ||||||
| 		auth: make(map[string]string), |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { |  | ||||||
| 	logg.TraceCtxf(a.Ctx, "looking for publickey", "pubkey", fmt.Sprintf("%x", pubKey)) |  | ||||||
| 	va, err := a.keyStore.Get(a.Ctx, pubKey) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	ka := hex.EncodeToString(conn.SessionID()) |  | ||||||
| 	a.auth[ka] = va  |  | ||||||
| 	fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va) |  | ||||||
| 	return nil, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(a *auther) FromConn(c *ssh.ServerConn) (string, error) { |  | ||||||
| 	if c == nil { |  | ||||||
| 		return "", errors.New("nil server conn") |  | ||||||
| 	} |  | ||||||
| 	if c.Conn == nil { |  | ||||||
| 		return "", errors.New("nil underlying conn") |  | ||||||
| 	} |  | ||||||
| 	return a.Get(c.Conn.SessionID()) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| func(a *auther) Get(k []byte) (string, error) { |  | ||||||
| 	ka := hex.EncodeToString(k) |  | ||||||
| 	v, ok := a.auth[ka] |  | ||||||
| 	if !ok { |  | ||||||
| 		return "", errors.New("not found") |  | ||||||
| 	} |  | ||||||
| 	return v, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type SshRunner struct { |  | ||||||
| 	Ctx context.Context |  | ||||||
| 	Cfg engine.Config |  | ||||||
| 	FlagFile string |  | ||||||
| 	Conn storage.ConnData |  | ||||||
| 	ResourceDir string |  | ||||||
| 	Debug bool |  | ||||||
| 	SrvKeyFile string |  | ||||||
| 	Host string |  | ||||||
| 	Port uint |  | ||||||
| 	wg sync.WaitGroup |  | ||||||
| 	lst net.Listener |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error { |  | ||||||
| 	if ch == nil { |  | ||||||
| 		return errors.New("nil channel") |  | ||||||
| 	} |  | ||||||
| 	if ch.ChannelType() != "session" { |  | ||||||
| 		ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for") |  | ||||||
| 		return errors.New("not a session") |  | ||||||
| 	} |  | ||||||
| 	channel, requests, err := ch.Accept() |  | ||||||
| 	if err != nil { |  | ||||||
| 		panic(err) |  | ||||||
| 	} |  | ||||||
| 	defer channel.Close() |  | ||||||
| 	s.wg.Add(1) |  | ||||||
| 	go func(reqIn <-chan *ssh.Request) { |  | ||||||
| 		defer s.wg.Done() |  | ||||||
| 		for req := range reqIn { |  | ||||||
| 			req.Reply(req.Type == "shell", nil)	 |  | ||||||
| 		} |  | ||||||
| 		_ = requests |  | ||||||
| 	}(requests) |  | ||||||
| 
 |  | ||||||
| 	cont, err := en.Exec(ctx, []byte{}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("initial engine exec err: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var input [state.INPUT_LIMIT]byte |  | ||||||
| 	for cont { |  | ||||||
| 		c, err := en.Flush(ctx, channel) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("flush err: %v", err) |  | ||||||
| 		} |  | ||||||
| 		_, err = channel.Write([]byte{0x0a}) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("newline err: %v", err) |  | ||||||
| 		} |  | ||||||
| 		c, err = channel.Read(input[:]) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("read input fail: %v", err) |  | ||||||
| 		} |  | ||||||
| 		logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1]) |  | ||||||
| 		cont, err = en.Exec(ctx, input[:c-1]) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("engine exec err: %v", err) |  | ||||||
| 		} |  | ||||||
| 		logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en) |  | ||||||
| 		_ = c |  | ||||||
| 	} |  | ||||||
| 	c, err := en.Flush(ctx, channel) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("last flush err: %v", err) |  | ||||||
| 	} |  | ||||||
| 	_ = c |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(s *SshRunner) Stop() error { |  | ||||||
| 	return s.lst.Close() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) { |  | ||||||
| 	ctx := s.Ctx |  | ||||||
| 	menuStorageService := storage.NewMenuStorageService(s.Conn, s.ResourceDir) |  | ||||||
| 
 |  | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	pe, err := menuStorageService.GetPersister(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	userdatastore, err := menuStorageService.GetUserdataDb(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	dbResource, ok := rs.(*resource.DbResource) |  | ||||||
| 	if !ok { |  | ||||||
| 		return nil, nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, s.FlagFile, true, dbResource, s.Cfg, rs) |  | ||||||
| 	lhs.SetDataStore(&userdatastore) |  | ||||||
| 	lhs.SetPersister(pe) |  | ||||||
| 	lhs.Cfg.SessionId = sessionId |  | ||||||
| 
 |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// TODO: clear up why pointer here and by-value other cmds
 |  | ||||||
| 	accountService := &remote.AccountService{} |  | ||||||
| 	hl, err := lhs.GetHandler(accountService) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	en := lhs.GetEngine() |  | ||||||
| 	en = en.WithFirst(hl.Init) |  | ||||||
| 	if s.Debug { |  | ||||||
| 		en = en.WithDebug(nil) |  | ||||||
| 	} |  | ||||||
| 	// TODO: this is getting very hacky!
 |  | ||||||
| 	closer := func() { |  | ||||||
| 		err := menuStorageService.Close() |  | ||||||
| 		if err != nil { |  | ||||||
| 			logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return en, closer, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // adapted example from crypto/ssh package, NewServerConn doc
 |  | ||||||
| func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) { |  | ||||||
| 	s.Ctx = ctx |  | ||||||
| 	running := true |  | ||||||
| 
 |  | ||||||
| 	// TODO: waitgroup should probably not be global
 |  | ||||||
| 	defer s.wg.Wait() |  | ||||||
| 
 |  | ||||||
| 	auth := NewAuther(ctx, keyStore) |  | ||||||
| 	cfg := ssh.ServerConfig{ |  | ||||||
| 		PublicKeyCallback: auth.Check, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	privateBytes, err := os.ReadFile(s.SrvKeyFile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		logg.ErrorCtxf(ctx, "Failed to load private key", "err", err) |  | ||||||
| 	} |  | ||||||
| 	private, err := ssh.ParsePrivateKey(privateBytes) |  | ||||||
| 	if err != nil { |  | ||||||
| 		logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err) |  | ||||||
| 	} |  | ||||||
| 	srvPub := private.PublicKey() |  | ||||||
| 	srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal()) |  | ||||||
| 	logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr) |  | ||||||
| 	cfg.AddHostKey(private) |  | ||||||
| 
 |  | ||||||
| 	s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		panic(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for running { |  | ||||||
| 		conn, err := s.lst.Accept() |  | ||||||
| 		if err != nil { |  | ||||||
| 			logg.ErrorCtxf(ctx, "ssh accept error", "err", err) |  | ||||||
| 			running = false |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		go func(conn net.Conn) { |  | ||||||
| 			defer conn.Close() |  | ||||||
| 			for true { |  | ||||||
| 				srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg) |  | ||||||
| 				if err != nil { |  | ||||||
| 					logg.InfoCtxf(ctx, "rejected client", "err", err) |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 				logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn) |  | ||||||
| 
 |  | ||||||
| 				s.wg.Add(1) |  | ||||||
| 				go func() { |  | ||||||
| 					ssh.DiscardRequests(rC) |  | ||||||
| 					s.wg.Done() |  | ||||||
| 				}() |  | ||||||
| 				 |  | ||||||
| 				sessionId, err := auth.FromConn(srvConn) |  | ||||||
| 				if err != nil { |  | ||||||
| 					logg.ErrorCtxf(ctx, "Cannot find authentication") |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 				en, closer, err := s.GetEngine(sessionId) |  | ||||||
| 				if err != nil { |  | ||||||
| 					logg.ErrorCtxf(ctx, "engine won't start", "err", err) |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 				defer func() { |  | ||||||
| 					err := en.Finish() |  | ||||||
| 					if err != nil { |  | ||||||
| 						logg.ErrorCtxf(ctx, "engine won't stop", "err", err) |  | ||||||
| 					} |  | ||||||
| 					closer() |  | ||||||
| 				}() |  | ||||||
| 				for ch := range nC { |  | ||||||
| 					err = s.serve(ctx, sessionId, ch, en) |  | ||||||
| 					logg.ErrorCtxf(ctx, "ssh server finish", "err", err) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}(conn) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,127 +0,0 @@ | |||||||
| package storage |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| 	gdbmdb "git.defalsify.org/vise.git/db/gdbm" |  | ||||||
| 	"git.defalsify.org/vise.git/lang" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg = logging.NewVanilla().WithDomain("gdbmstorage") |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	dbC map[string]chan db.Db |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type ThreadGdbmDb struct { |  | ||||||
| 	db db.Db |  | ||||||
| 	connStr string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewThreadGdbmDb() *ThreadGdbmDb { |  | ||||||
| 	if dbC == nil { |  | ||||||
| 		dbC = make(map[string]chan db.Db) |  | ||||||
| 	} |  | ||||||
| 	return &ThreadGdbmDb{} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) Connect(ctx context.Context, connStr string) error { |  | ||||||
| 	var ok bool |  | ||||||
| 	_, ok = dbC[connStr] |  | ||||||
| 	if ok { |  | ||||||
| 		logg.WarnCtxf(ctx, "already registered thread gdbm, skipping", "connStr", connStr) |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	gdb := gdbmdb.NewGdbmDb() |  | ||||||
| 	err := gdb.Connect(ctx, connStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	dbC[connStr] = make(chan db.Db, 1) |  | ||||||
| 	dbC[connStr]<- gdb |  | ||||||
| 	tdb.connStr = connStr |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) reserve() { |  | ||||||
| 	if tdb.db == nil { |  | ||||||
| 		tdb.db = <-dbC[tdb.connStr] |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) release() { |  | ||||||
| 	if tdb.db == nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	dbC[tdb.connStr] <- tdb.db |  | ||||||
| 	tdb.db = nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) SetPrefix(pfx uint8) { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	tdb.db.SetPrefix(pfx) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) SetSession(sessionId string) { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	tdb.db.SetSession(sessionId) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) SetLanguage(lng *lang.Language) { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	tdb.db.SetLanguage(lng) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) Safe() bool { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	v := tdb.db.Safe() |  | ||||||
| 	tdb.release() |  | ||||||
| 	return v |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) Prefix() uint8 { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	v := tdb.db.Prefix() |  | ||||||
| 	tdb.release() |  | ||||||
| 	return v |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) SetLock(typ uint8, locked bool) error { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	err := tdb.db.SetLock(typ, locked) |  | ||||||
| 	tdb.release() |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) Put(ctx context.Context, key []byte, val []byte) error { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	err := tdb.db.Put(ctx, key, val) |  | ||||||
| 	tdb.release() |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) Get(ctx context.Context, key []byte) ([]byte, error) { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	v, err := tdb.db.Get(ctx, key) |  | ||||||
| 	tdb.release() |  | ||||||
| 	return v, err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) Close() error { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	close(dbC[tdb.connStr]) |  | ||||||
| 	delete(dbC, tdb.connStr) |  | ||||||
| 	err := tdb.db.Close() |  | ||||||
| 	tdb.db = nil |  | ||||||
| 	return err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(tdb *ThreadGdbmDb) Dump(ctx context.Context, key []byte) (*db.Dumper, error) { |  | ||||||
| 	tdb.reserve() |  | ||||||
| 	defer tdb.release() |  | ||||||
| 	return tdb.db.Dump(ctx, key) |  | ||||||
| } |  | ||||||
| @ -1,43 +0,0 @@ | |||||||
| package storage |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // PrefixDb interface abstracts the database operations.
 |  | ||||||
| type PrefixDb interface { |  | ||||||
| 	Get(ctx context.Context, key []byte) ([]byte, error) |  | ||||||
| 	Put(ctx context.Context, key []byte, val []byte) error |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var _ PrefixDb = (*SubPrefixDb)(nil) |  | ||||||
| 
 |  | ||||||
| type SubPrefixDb struct { |  | ||||||
| 	store db.Db |  | ||||||
| 	pfx   []byte |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb { |  | ||||||
| 	return &SubPrefixDb{ |  | ||||||
| 		store: store, |  | ||||||
| 		pfx:   pfx, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *SubPrefixDb) toKey(k []byte) []byte { |  | ||||||
| 	return append(s.pfx, k...) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { |  | ||||||
| 	s.store.SetPrefix(db.DATATYPE_USERDATA) |  | ||||||
| 	key = s.toKey(key) |  | ||||||
| 	return s.store.Get(ctx, key) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { |  | ||||||
| 	s.store.SetPrefix(db.DATATYPE_USERDATA) |  | ||||||
| 	key = s.toKey(key) |  | ||||||
| 	return s.store.Put(ctx, key, val) |  | ||||||
| } |  | ||||||
| @ -1,54 +0,0 @@ | |||||||
| package storage |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"context" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	memdb "git.defalsify.org/vise.git/db/mem" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestSubPrefix(t *testing.T) { |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	db := memdb.NewMemDb() |  | ||||||
| 	err := db.Connect(ctx, "") |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	sdba := NewSubPrefixDb(db, []byte("tinkywinky")) |  | ||||||
| 	err = sdba.Put(ctx, []byte("foo"), []byte("dipsy")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	r, err := sdba.Get(ctx, []byte("foo")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	if !bytes.Equal(r, []byte("dipsy")) { |  | ||||||
| 		t.Fatalf("expected 'dipsy', got %s", r) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	sdbb := NewSubPrefixDb(db, []byte("lala")) |  | ||||||
| 	r, err = sdbb.Get(ctx, []byte("foo")) |  | ||||||
| 	if err == nil { |  | ||||||
| 		t.Fatal("expected not found") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err = sdbb.Put(ctx, []byte("foo"), []byte("pu")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	r, err = sdbb.Get(ctx, []byte("foo")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	if !bytes.Equal(r, []byte("pu")) { |  | ||||||
| 		t.Fatalf("expected 'pu', got %s", r) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	r, err = sdba.Get(ctx, []byte("foo")) |  | ||||||
| 	if !bytes.Equal(r, []byte("dipsy")) { |  | ||||||
| 		t.Fatalf("expected 'dipsy', got %s", r) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,86 +0,0 @@ | |||||||
| package storage |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"net/url" |  | ||||||
| 	"path" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	DBTYPE_MEM = iota |  | ||||||
| 	DBTYPE_GDBM |  | ||||||
| 	DBTYPE_POSTGRES |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type ConnData struct { |  | ||||||
| 	typ int |  | ||||||
| 	str string |  | ||||||
| 	domain string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (cd *ConnData) DbType() int { |  | ||||||
| 	return cd.typ |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (cd *ConnData) String() string { |  | ||||||
| 	return cd.str |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (cd *ConnData) Domain() string { |  | ||||||
| 	return cd.domain |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (cd *ConnData) Path() string { |  | ||||||
| 	v, _ := url.Parse(cd.str) |  | ||||||
| 	v.RawQuery = "" |  | ||||||
| 	return v.String() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func probePostgres(s string) (string, string, bool) { |  | ||||||
| 	domain := "public" |  | ||||||
| 	v, err := url.Parse(s) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", "", false |  | ||||||
| 	} |  | ||||||
| 	if v.Scheme != "postgres" { |  | ||||||
| 		return "", "", false |  | ||||||
| 	} |  | ||||||
| 	vv := v.Query() |  | ||||||
| 	if vv.Has("search_path") { |  | ||||||
| 		domain = vv.Get("search_path") |  | ||||||
| 	} |  | ||||||
| 	return s, domain, true |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func probeGdbm(s string) (string, string, bool) { |  | ||||||
| 	if !path.IsAbs(s) { |  | ||||||
| 		return "", "", false |  | ||||||
| 	} |  | ||||||
| 	s = path.Clean(s) |  | ||||||
| 	return s, "", true |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func ToConnData(connStr string) (ConnData, error) { |  | ||||||
| 	var o ConnData |  | ||||||
| 
 |  | ||||||
| 	if connStr == "" { |  | ||||||
| 		return o, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	v, domain, ok := probePostgres(connStr) |  | ||||||
| 	if ok { |  | ||||||
| 		o.typ = DBTYPE_POSTGRES |  | ||||||
| 		o.str = v |  | ||||||
| 		o.domain = domain |  | ||||||
| 		return o, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	v, _, ok = probeGdbm(connStr) |  | ||||||
| 	if ok { |  | ||||||
| 		o.typ = DBTYPE_GDBM |  | ||||||
| 		o.str = v |  | ||||||
| 		return o, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return o, fmt.Errorf("invalid connection string: %s", connStr) |  | ||||||
| } |  | ||||||
| @ -1,28 +0,0 @@ | |||||||
| package storage |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestParseConnStr(t *testing.T) { |  | ||||||
| 	_, err := ToConnData("postgres://foo:bar@localhost:5432/baz") |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err)	 |  | ||||||
| 	} |  | ||||||
| 	_, err = ToConnData("/foo/bar") |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err)	 |  | ||||||
| 	} |  | ||||||
| 	_, err = ToConnData("/foo/bar/") |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err)	 |  | ||||||
| 	} |  | ||||||
| 	_, err = ToConnData("foo/bar") |  | ||||||
| 	if err == nil { |  | ||||||
| 		t.Fatalf("expected error") |  | ||||||
| 	} |  | ||||||
| 	_, err = ToConnData("http://foo/bar") |  | ||||||
| 	if err == nil { |  | ||||||
| 		t.Fatalf("expected error") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -5,10 +5,6 @@ import ( | |||||||
| 	"git.defalsify.org/vise.git/persist" | 	"git.defalsify.org/vise.git/persist" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( |  | ||||||
| 	DATATYPE_EXTEND = 128 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type Storage struct { | type Storage struct { | ||||||
| 	Persister *persist.Persister | 	Persister *persist.Persister | ||||||
| 	UserdataDb db.Db	 | 	UserdataDb db.Db	 | ||||||
|  | |||||||
| @ -1,192 +0,0 @@ | |||||||
| package storage |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| 	fsdb "git.defalsify.org/vise.git/db/fs" |  | ||||||
| 	"git.defalsify.org/vise.git/db/postgres" |  | ||||||
| 	"git.defalsify.org/vise.git/lang" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.defalsify.org/vise.git/persist" |  | ||||||
| 	"git.defalsify.org/vise.git/resource" |  | ||||||
| 	gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm" |  | ||||||
| 	"github.com/jackc/pgx/v5/pgxpool" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg = logging.NewVanilla().WithDomain("storage") |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type StorageService interface { |  | ||||||
| 	GetPersister(ctx context.Context) (*persist.Persister, error) |  | ||||||
| 	GetUserdataDb(ctx context.Context) db.Db |  | ||||||
| 	GetResource(ctx context.Context) (resource.Resource, error) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type MenuStorageService struct { |  | ||||||
| 	conn ConnData |  | ||||||
| 	resourceDir   string |  | ||||||
| 	poResource    resource.Resource |  | ||||||
| 	resourceStore db.Db |  | ||||||
| 	stateStore    db.Db |  | ||||||
| 	userDataStore db.Db |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageService { |  | ||||||
| 	return &MenuStorageService{ |  | ||||||
| 		conn: conn, |  | ||||||
| 		resourceDir: resourceDir, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) { |  | ||||||
| 	var newDb db.Db |  | ||||||
| 	var err error |  | ||||||
| 
 |  | ||||||
| 	if existingDb != nil { |  | ||||||
| 		return existingDb, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 	connStr := ms.conn.String() |  | ||||||
| 	dbTyp := ms.conn.DbType() |  | ||||||
| 	if dbTyp == DBTYPE_POSTGRES { |  | ||||||
| 		// TODO: move to vise
 |  | ||||||
| 		err = ensureSchemaExists(ctx, ms.conn) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		newDb = postgres.NewPgDb().WithSchema(ms.conn.Domain()) |  | ||||||
| 	} else if dbTyp == DBTYPE_GDBM { |  | ||||||
| 		err = ms.ensureDbDir() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		connStr = path.Join(connStr, section) |  | ||||||
| 		newDb = gdbmstorage.NewThreadGdbmDb() |  | ||||||
| 	} else { |  | ||||||
| 		return nil, fmt.Errorf("unsupported connection string: '%s'\n", ms.conn.String()) |  | ||||||
| 	} |  | ||||||
| 	logg.DebugCtxf(ctx, "connecting to db", "conn", connStr, "conndata", ms.conn) |  | ||||||
| 	err = newDb.Connect(ctx, connStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return newDb, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // WithGettext triggers use of gettext for translation of templates and menus.
 |  | ||||||
| //
 |  | ||||||
| // The first language in `lns` will be used as default language, to resolve node keys to 
 |  | ||||||
| // language strings.
 |  | ||||||
| //
 |  | ||||||
| // If `lns` is an empty array, gettext will not be used.
 |  | ||||||
| func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *MenuStorageService { |  | ||||||
| 	if len(lns) == 0 { |  | ||||||
| 		logg.Warnf("Gettext requested but no languages supplied") |  | ||||||
| 		return ms |  | ||||||
| 	} |  | ||||||
| 	rs := resource.NewPoResource(lns[0], path) |  | ||||||
| 
 |  | ||||||
| 	for _, ln := range(lns) { |  | ||||||
| 		rs = rs.WithLanguage(ln) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	ms.poResource = rs |  | ||||||
| 
 |  | ||||||
| 	return ms |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // ensureSchemaExists creates a new schema if it does not exist
 |  | ||||||
| func ensureSchemaExists(ctx context.Context, conn ConnData) error { |  | ||||||
| 	h, err := pgxpool.New(ctx, conn.Path()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to connect to the database: %w", err) |  | ||||||
| 	} |  | ||||||
| 	defer h.Close() |  | ||||||
| 
 |  | ||||||
| 	query := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", conn.Domain()) |  | ||||||
| 	_, err = h.Exec(ctx, query) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to create schema: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { |  | ||||||
| 	stateStore, err := ms.GetStateStore(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	pr := persist.NewPersister(stateStore) |  | ||||||
| 	logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", stateStore) |  | ||||||
| 	return pr, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *MenuStorageService) GetUserdataDb(ctx context.Context) (db.Db, error) { |  | ||||||
| 	if ms.userDataStore != nil { |  | ||||||
| 		return ms.userDataStore, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	userDataStore, err := ms.getOrCreateDb(ctx, ms.userDataStore, "userdata.gdbm") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	ms.userDataStore = userDataStore |  | ||||||
| 	return ms.userDataStore, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resource, error) { |  | ||||||
| 	ms.resourceStore = fsdb.NewFsDb() |  | ||||||
| 	err := ms.resourceStore.Connect(ctx, ms.resourceDir) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	rfs := resource.NewDbResource(ms.resourceStore) |  | ||||||
| 	if ms.poResource != nil { |  | ||||||
| 		logg.InfoCtxf(ctx, "using poresource for menu and template") |  | ||||||
| 		rfs.WithMenuGetter(ms.poResource.GetMenu) |  | ||||||
| 		rfs.WithTemplateGetter(ms.poResource.GetTemplate) |  | ||||||
| 	} |  | ||||||
| 	return rfs, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) { |  | ||||||
| 	if ms.stateStore != nil { |  | ||||||
| 		return ms.stateStore, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	stateStore, err := ms.getOrCreateDb(ctx, ms.stateStore, "state.gdbm") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	ms.stateStore = stateStore |  | ||||||
| 	return ms.stateStore, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *MenuStorageService) ensureDbDir() error { |  | ||||||
| 	err := os.MkdirAll(ms.conn.String(), 0700) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("state dir create exited with error: %v\n", err) |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (ms *MenuStorageService) Close() error { |  | ||||||
| 	errA := ms.stateStore.Close() |  | ||||||
| 	errB := ms.userDataStore.Close() |  | ||||||
| 	errC := ms.resourceStore.Close() |  | ||||||
| 	if errA != nil || errB != nil || errC != nil { |  | ||||||
| 		return fmt.Errorf("%v %v %v", errA, errB, errC) |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @ -1,111 +0,0 @@ | |||||||
| package driver |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"log" |  | ||||||
| 	"os" |  | ||||||
| 	"regexp" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type Step struct { |  | ||||||
| 	Input           string `json:"input"` |  | ||||||
| 	ExpectedContent string `json:"expectedContent"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Step) MatchesExpectedContent(content []byte) (bool, error) { |  | ||||||
| 	pattern := regexp.QuoteMeta(s.ExpectedContent) |  | ||||||
| 	re, err := regexp.Compile(pattern) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, err |  | ||||||
| 	} |  | ||||||
| 	if re.Match([]byte(content)) { |  | ||||||
| 		return true, nil |  | ||||||
| 	} |  | ||||||
| 	return false, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Group represents a group of steps
 |  | ||||||
| type Group struct { |  | ||||||
| 	Name  string `json:"name"` |  | ||||||
| 	Steps []Step `json:"steps"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type TestCase struct { |  | ||||||
| 	Name            string |  | ||||||
| 	Input           string |  | ||||||
| 	ExpectedContent string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *TestCase) MatchesExpectedContent(content []byte) (bool, error) { |  | ||||||
| 	pattern := regexp.QuoteMeta(s.ExpectedContent) |  | ||||||
| 	re, err := regexp.Compile(pattern) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, err |  | ||||||
| 	} |  | ||||||
| 	// Check if the content matches the regex pattern
 |  | ||||||
| 	if re.Match(content) { |  | ||||||
| 		return true, nil |  | ||||||
| 	} |  | ||||||
| 	return false, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // DataGroup represents the overall structure of the JSON.
 |  | ||||||
| type DataGroup struct { |  | ||||||
| 	Groups []Group `json:"groups"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Session struct { |  | ||||||
| 	Name   string  `json:"name"` |  | ||||||
| 	Groups []Group `json:"groups"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func ReadData() []Session { |  | ||||||
| 	data, err := os.ReadFile("test_setup.json") |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatalf("Failed to read file: %v", err) |  | ||||||
| 	} |  | ||||||
| 	// Unmarshal JSON data
 |  | ||||||
| 	var sessions []Session |  | ||||||
| 	err = json.Unmarshal(data, &sessions) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatalf("Failed to unmarshal JSON: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return sessions |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func FilterGroupsByName(groups []Group, name string) []Group { |  | ||||||
| 	var filteredGroups []Group |  | ||||||
| 	for _, group := range groups { |  | ||||||
| 		if group.Name == name { |  | ||||||
| 			filteredGroups = append(filteredGroups, group) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return filteredGroups |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func LoadTestGroups(filePath string) (DataGroup, error) { |  | ||||||
| 	var sessionsData DataGroup |  | ||||||
| 	data, err := os.ReadFile(filePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return sessionsData, err |  | ||||||
| 	} |  | ||||||
| 	err = json.Unmarshal(data, &sessionsData) |  | ||||||
| 	return sessionsData, err |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func CreateTestCases(group DataGroup) []TestCase { |  | ||||||
| 	var tests []TestCase |  | ||||||
| 	for _, group := range group.Groups { |  | ||||||
| 		for _, step := range group.Steps { |  | ||||||
| 			// Create a test case for each group
 |  | ||||||
| 			tests = append(tests, TestCase{ |  | ||||||
| 				Name:            group.Name, |  | ||||||
| 				Input:           step.Input, |  | ||||||
| 				ExpectedContent: step.ExpectedContent, |  | ||||||
| 			}) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return tests |  | ||||||
| } |  | ||||||
| @ -1,209 +0,0 @@ | |||||||
| package testutil |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"log" |  | ||||||
| 	"net/url" |  | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/engine" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| 	"git.defalsify.org/vise.git/resource" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/config" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/testservice" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/testtag" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" |  | ||||||
| 	"github.com/jackc/pgx/v5/pgxpool" |  | ||||||
| 	testdataloader "github.com/peteole/testdata-loader" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg        = logging.NewVanilla() |  | ||||||
| 	baseDir     = testdataloader.GetBasePath() |  | ||||||
| 	scriptDir   = path.Join(baseDir, "services", "registration") |  | ||||||
| 	setDbType   string |  | ||||||
| 	setConnStr  string |  | ||||||
| 	setDbSchema string |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	initializers.LoadEnvVariablesPath(baseDir) |  | ||||||
| 	config.LoadConfig() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // SetDatabase updates the database used by TestEngine
 |  | ||||||
| func SetDatabase(database, connStr, dbSchema string) { |  | ||||||
| 	setDbType = database |  | ||||||
| 	setConnStr = connStr |  | ||||||
| 	setDbSchema = dbSchema |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // CleanDatabase removes all test data from the database
 |  | ||||||
| func CleanDatabase() { |  | ||||||
| 	if setDbType == "postgres" { |  | ||||||
| 		ctx := context.Background() |  | ||||||
| 		// Update the connection string with the new search path
 |  | ||||||
| 		updatedConnStr, err := updateSearchPath(setConnStr, setDbSchema) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Fatalf("Failed to update search path: %v", err) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		dbConn, err := pgxpool.New(ctx, updatedConnStr) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Fatalf("Failed to connect to database for cleanup: %v", err) |  | ||||||
| 		} |  | ||||||
| 		defer dbConn.Close() |  | ||||||
| 
 |  | ||||||
| 		query := fmt.Sprintf("DELETE FROM %s.kv_vise;", setDbSchema) |  | ||||||
| 		_, execErr := dbConn.Exec(ctx, query) |  | ||||||
| 		if execErr != nil { |  | ||||||
| 			log.Printf("Failed to cleanup table %s.kv_vise: %v", setDbSchema, execErr) |  | ||||||
| 		} else { |  | ||||||
| 			log.Printf("Successfully cleaned up table %s.kv_vise", setDbSchema) |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		setConnStr, _ := filepath.Abs(setConnStr) |  | ||||||
| 		if err := os.RemoveAll(setConnStr); err != nil { |  | ||||||
| 			log.Fatalf("Failed to delete state store %s: %v", setConnStr, err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // updateSearchPath updates the search_path (schema) to be used in the connection
 |  | ||||||
| func updateSearchPath(connStr string, newSearchPath string) (string, error) { |  | ||||||
| 	u, err := url.Parse(connStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", fmt.Errorf("invalid connection string: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Parse the query parameters
 |  | ||||||
| 	q := u.Query() |  | ||||||
| 
 |  | ||||||
| 	// Update or add the search_path parameter
 |  | ||||||
| 	q.Set("search_path", newSearchPath) |  | ||||||
| 
 |  | ||||||
| 	// Rebuild the connection string with updated parameters
 |  | ||||||
| 	u.RawQuery = q.Encode() |  | ||||||
| 
 |  | ||||||
| 	return u.String(), nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { |  | ||||||
| 	var err error |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) |  | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") |  | ||||||
| 
 |  | ||||||
| 	var eventChannel = make(chan bool) |  | ||||||
| 
 |  | ||||||
| 	cfg := engine.Config{ |  | ||||||
| 		Root:       "root", |  | ||||||
| 		SessionId:  sessionId, |  | ||||||
| 		OutputSize: uint32(160), |  | ||||||
| 		FlagCount:  uint32(128), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if setDbType == "postgres" { |  | ||||||
| 		setConnStr = config.DbConn |  | ||||||
| 		setConnStr, err = updateSearchPath(setConnStr, setDbSchema) |  | ||||||
| 		if err != nil { |  | ||||||
| 			fmt.Println("Error:", err) |  | ||||||
| 			os.Exit(1) |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		setConnStr, err = filepath.Abs(setConnStr) |  | ||||||
| 		if err != nil { |  | ||||||
| 			fmt.Fprintf(os.Stderr, "connstr err: %v", err) |  | ||||||
| 			os.Exit(1) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	conn, err := storage.ToConnData(setConnStr) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "connstr parse err: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	resourceDir := scriptDir |  | ||||||
| 	menuStorageService := storage.NewMenuStorageService(conn, resourceDir) |  | ||||||
| 
 |  | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "resource error: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	pe, err := menuStorageService.GetPersister(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "persister error: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	userDataStore, err := menuStorageService.GetUserdataDb(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "userdb error: %v", err) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	dbResource, ok := rs.(*resource.DbResource) |  | ||||||
| 	if !ok { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "dbresource cast error") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) |  | ||||||
| 	lhs.SetDataStore(&userDataStore) |  | ||||||
| 	lhs.SetPersister(pe) |  | ||||||
| 
 |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if testtag.AccountService == nil { |  | ||||||
| 		testtag.AccountService = &remote.AccountService{} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	switch testtag.AccountService.(type) { |  | ||||||
| 	case *testservice.TestAccountService: |  | ||||||
| 		go func() { |  | ||||||
| 			eventChannel <- false |  | ||||||
| 		}() |  | ||||||
| 	case *remote.AccountService: |  | ||||||
| 		go func() { |  | ||||||
| 			time.Sleep(5 * time.Second) // Wait for 5 seconds
 |  | ||||||
| 			eventChannel <- true |  | ||||||
| 		}() |  | ||||||
| 	default: |  | ||||||
| 		panic("Unknown account service type") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	hl, err := lhs.GetHandler(testtag.AccountService) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	en := lhs.GetEngine() |  | ||||||
| 	en = en.WithFirst(hl.Init) |  | ||||||
| 	cleanFn := func() { |  | ||||||
| 		err := en.Finish() |  | ||||||
| 		if err != nil { |  | ||||||
| 			logg.Errorf(err.Error()) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		err = menuStorageService.Close() |  | ||||||
| 		if err != nil { |  | ||||||
| 			logg.Errorf(err.Error()) |  | ||||||
| 		} |  | ||||||
| 		logg.Infof("testengine storage closed") |  | ||||||
| 	} |  | ||||||
| 	return en, cleanFn, eventChannel |  | ||||||
| } |  | ||||||
| @ -1,15 +0,0 @@ | |||||||
| package testutil |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"testing" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func TestCreateEngine(t *testing.T) { |  | ||||||
| 	o, clean, eventC := TestEngine("foo") |  | ||||||
| 	defer clean() |  | ||||||
| 	defer func() { |  | ||||||
| 		<-eventC |  | ||||||
| 		close(eventC) |  | ||||||
| 	}() |  | ||||||
| 	_ = o |  | ||||||
| } |  | ||||||
| @ -1,30 +0,0 @@ | |||||||
| package httpmocks |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"io" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // MockEngine implements the engine.Engine interface for testing
 |  | ||||||
| type MockEngine struct { |  | ||||||
| 	InitFunc   func(context.Context) (bool, error) |  | ||||||
| 	ExecFunc   func(context.Context, []byte) (bool, error) |  | ||||||
| 	FlushFunc  func(context.Context, io.Writer) (int, error) |  | ||||||
| 	FinishFunc func() error |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockEngine) Init(ctx context.Context) (bool, error) { |  | ||||||
| 	return m.InitFunc(ctx) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockEngine) Exec(ctx context.Context, input []byte) (bool, error) { |  | ||||||
| 	return m.ExecFunc(ctx, input) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockEngine) Flush(ctx context.Context, w io.Writer) (int, error) { |  | ||||||
| 	return m.FlushFunc(ctx, w) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockEngine) Finish() error { |  | ||||||
| 	return m.FinishFunc() |  | ||||||
| } |  | ||||||
| @ -1,47 +0,0 @@ | |||||||
| package httpmocks |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"git.defalsify.org/vise.git/engine" |  | ||||||
| 	"git.defalsify.org/vise.git/persist" |  | ||||||
| 	"git.defalsify.org/vise.git/resource" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // MockRequestHandler implements handlers.RequestHandler interface for testing
 |  | ||||||
| type MockRequestHandler struct { |  | ||||||
| 	ProcessFunc          func(handlers.RequestSession) (handlers.RequestSession, error) |  | ||||||
| 	GetConfigFunc        func() engine.Config |  | ||||||
| 	GetEngineFunc        func(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine |  | ||||||
| 	OutputFunc           func(rs handlers.RequestSession) (handlers.RequestSession, error) |  | ||||||
| 	ResetFunc            func(rs handlers.RequestSession) (handlers.RequestSession, error) |  | ||||||
| 	ShutdownFunc         func() |  | ||||||
| 	GetRequestParserFunc func() handlers.RequestParser |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockRequestHandler) Process(rqs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 	return m.ProcessFunc(rqs) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockRequestHandler) GetConfig() engine.Config { |  | ||||||
| 	return m.GetConfigFunc() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine { |  | ||||||
| 	return m.GetEngineFunc(cfg, rs, pe) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockRequestHandler) Output(rs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 	return m.OutputFunc(rs) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockRequestHandler) Reset(rs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 	return m.ResetFunc(rs) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockRequestHandler) Shutdown() { |  | ||||||
| 	m.ShutdownFunc() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockRequestHandler) GetRequestParser() handlers.RequestParser { |  | ||||||
| 	return m.GetRequestParserFunc() |  | ||||||
| } |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| package httpmocks |  | ||||||
| 
 |  | ||||||
| import "context" |  | ||||||
| 
 |  | ||||||
| // MockRequestParser implements the handlers.RequestParser interface for testing
 |  | ||||||
| type MockRequestParser struct { |  | ||||||
| 	GetSessionIdFunc func(any) (string, error) |  | ||||||
| 	GetInputFunc     func(any) ([]byte, error) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) { |  | ||||||
| 	return m.GetSessionIdFunc(rq) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockRequestParser) GetInput(rq any) ([]byte, error) { |  | ||||||
| 	return m.GetInputFunc(rq) |  | ||||||
| } |  | ||||||
| @ -1,25 +0,0 @@ | |||||||
| package httpmocks |  | ||||||
| 
 |  | ||||||
| import "net/http" |  | ||||||
| 
 |  | ||||||
| // MockWriter implements a mock io.Writer for testing
 |  | ||||||
| type MockWriter struct { |  | ||||||
| 	WriteStringCalled bool |  | ||||||
| 	WrittenString     string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockWriter) Write(p []byte) (n int, err error) { |  | ||||||
| 	return len(p), nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockWriter) WriteString(s string) (n int, err error) { |  | ||||||
| 	m.WriteStringCalled = true |  | ||||||
| 	m.WrittenString = s |  | ||||||
| 	return len(s), nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockWriter) Header() http.Header { |  | ||||||
| 	return http.Header{} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockWriter) WriteHeader(statusCode int) {} |  | ||||||
| @ -1,54 +0,0 @@ | |||||||
| package mocks |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/models" |  | ||||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" |  | ||||||
| 	"github.com/stretchr/testify/mock" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // MockAccountService implements AccountServiceInterface for testing
 |  | ||||||
| type MockAccountService struct { |  | ||||||
| 	mock.Mock |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { |  | ||||||
| 	args := m.Called() |  | ||||||
| 	return args.Get(0).(*models.AccountResult), args.Error(1) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { |  | ||||||
| 	args := m.Called(publicKey) |  | ||||||
| 	return args.Get(0).(*models.BalanceResult), args.Error(1) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) { |  | ||||||
| 	args := m.Called(trackingId) |  | ||||||
| 	return args.Get(0).(*models.TrackStatusResult), args.Error(1) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { |  | ||||||
| 	args := m.Called(publicKey) |  | ||||||
| 	return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { |  | ||||||
| 	args := m.Called(publicKey) |  | ||||||
| 	return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { |  | ||||||
| 	args := m.Called(address) |  | ||||||
| 	return args.Get(0).(*models.VoucherDataResult), args.Error(1) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { |  | ||||||
| 	args := m.Called() |  | ||||||
| 	return args.Get(0).(*models.TokenTransferResponse), args.Error(1) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { |  | ||||||
| 	args := m.Called(alias) |  | ||||||
| 	return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1) |  | ||||||
| } |  | ||||||
| @ -1,62 +0,0 @@ | |||||||
| package testservice |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/models" |  | ||||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type TestAccountService struct { |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { |  | ||||||
| 	return &models.AccountResult{ |  | ||||||
| 		TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d", |  | ||||||
| 		PublicKey:  "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD", |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { |  | ||||||
| 	balanceResponse := &models.BalanceResult{ |  | ||||||
| 		Balance: "0.003 CELO", |  | ||||||
| 		Nonce:   json.Number("0"), |  | ||||||
| 	} |  | ||||||
| 	return balanceResponse, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { |  | ||||||
| 	return &models.TrackStatusResult{ |  | ||||||
| 		Active: true, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { |  | ||||||
| 	return []dataserviceapi.TokenHoldings{ |  | ||||||
| 		dataserviceapi.TokenHoldings{ |  | ||||||
| 			ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", |  | ||||||
| 			TokenSymbol:     "SRF", |  | ||||||
| 			TokenDecimals:   "6", |  | ||||||
| 			Balance:         "2745987", |  | ||||||
| 		}, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { |  | ||||||
| 	return []dataserviceapi.Last10TxResponse{}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m TestAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { |  | ||||||
| 	return &models.VoucherDataResult{}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { |  | ||||||
| 	return &models.TokenTransferResponse{ |  | ||||||
| 		TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af", |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { |  | ||||||
| 	return &dataserviceapi.AliasAddress{}, nil |  | ||||||
| } |  | ||||||
| @ -1,12 +0,0 @@ | |||||||
| // +build !online
 |  | ||||||
| 
 |  | ||||||
| package testtag |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" |  | ||||||
| 	accountservice "git.grassecon.net/urdt/ussd/internal/testutil/testservice" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	AccountService remote.AccountServiceInterface = &accountservice.TestAccountService{} |  | ||||||
| ) |  | ||||||
| @ -1,9 +0,0 @@ | |||||||
| // +build online
 |  | ||||||
| 
 |  | ||||||
| package testtag |  | ||||||
| 
 |  | ||||||
| import "git.grassecon.net/urdt/ussd/remote" |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	AccountService remote.AccountServiceInterface |  | ||||||
| ) |  | ||||||
| @ -1,51 +0,0 @@ | |||||||
| package utils |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 
 |  | ||||||
| 	"git.defalsify.org/vise.git/db" |  | ||||||
| 	fsdb "git.defalsify.org/vise.git/db/fs" |  | ||||||
| 	"git.defalsify.org/vise.git/logging" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	logg = logging.NewVanilla().WithDomain("adminstore") |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type AdminStore struct { |  | ||||||
| 	ctx     context.Context |  | ||||||
| 	FsStore db.Db |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewAdminStore(ctx context.Context, fileName string) (*AdminStore, error) { |  | ||||||
| 	fsStore, err := getFsStore(ctx, fileName) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return &AdminStore{ctx: ctx, FsStore: fsStore}, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func getFsStore(ctx context.Context, connectStr string) (db.Db, error) { |  | ||||||
| 	fsStore := fsdb.NewFsDb() |  | ||||||
| 	err := fsStore.Connect(ctx, connectStr) |  | ||||||
| 	fsStore.SetPrefix(db.DATATYPE_USERDATA) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return fsStore, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Checks if the given sessionId is listed as an admin.
 |  | ||||||
| func (as *AdminStore) IsAdmin(sessionId string) (bool, error) { |  | ||||||
| 	_, err := as.FsStore.Get(as.ctx, []byte(sessionId)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if db.IsNotFound(err) { |  | ||||||
| 			logg.Printf(logging.LVL_INFO, "Returning false because session id was not found") |  | ||||||
| 			return false, nil |  | ||||||
| 		} else { |  | ||||||
| 			return false, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return true, nil |  | ||||||
| } |  | ||||||
| @ -1,9 +1,6 @@ | |||||||
| package utils | package utils | ||||||
| 
 | 
 | ||||||
| import ( | import "time" | ||||||
| 	"strconv" |  | ||||||
| 	"time" |  | ||||||
| ) |  | ||||||
| 
 | 
 | ||||||
| // CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
 | // CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
 | ||||||
| // It adjusts for cases where the current date is before the birthday in the current year.
 | // It adjusts for cases where the current date is before the birthday in the current year.
 | ||||||
| @ -28,29 +25,11 @@ func CalculateAge(birthdate, today time.Time) int { | |||||||
| // It subtracts the YOB from the current year to determine the age.
 | // It subtracts the YOB from the current year to determine the age.
 | ||||||
| //
 | //
 | ||||||
| // Parameters:
 | // Parameters:
 | ||||||
| //
 | //   yob: The year of birth as an integer.
 | ||||||
| //	yob: The year of birth as an integer.
 |  | ||||||
| //
 | //
 | ||||||
| // Returns:
 | // Returns:
 | ||||||
| //
 | //   The calculated age as an integer.
 | ||||||
| //	The calculated age as an integer.
 |  | ||||||
| func CalculateAgeWithYOB(yob int) int { | func CalculateAgeWithYOB(yob int) int { | ||||||
| 	currentYear := time.Now().Year() |     currentYear := time.Now().Year() | ||||||
| 	return currentYear - yob |     return currentYear - yob | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| //IsValidYob checks if the provided yob can be considered valid 
 |  | ||||||
| func IsValidYOb(yob string) bool { |  | ||||||
| 	currentYear := time.Now().Year() |  | ||||||
| 	yearOfBirth, err := strconv.ParseInt(yob, 10, 64) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	if yearOfBirth >= 1900 && int(yearOfBirth) <= currentYear { |  | ||||||
| 		return true |  | ||||||
| 	} else { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
							
								
								
									
										37
									
								
								internal/utils/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								internal/utils/db.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | package utils | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type DataTyp uint16 | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	DATA_ACCOUNT DataTyp = iota | ||||||
|  | 	DATA_ACCOUNT_CREATED | ||||||
|  | 	DATA_TRACKING_ID | ||||||
|  | 	DATA_PUBLIC_KEY | ||||||
|  | 	DATA_CUSTODIAL_ID | ||||||
|  | 	DATA_ACCOUNT_PIN | ||||||
|  | 	DATA_ACCOUNT_STATUS | ||||||
|  | 	DATA_FIRST_NAME | ||||||
|  | 	DATA_FAMILY_NAME | ||||||
|  | 	DATA_YOB | ||||||
|  | 	DATA_LOCATION | ||||||
|  | 	DATA_GENDER | ||||||
|  | 	DATA_OFFERINGS | ||||||
|  | 	DATA_RECIPIENT | ||||||
|  | 	DATA_AMOUNT | ||||||
|  | 	DATA_TEMPORARY_PIN | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func typToBytes(typ DataTyp) []byte { | ||||||
|  | 	var b [2]byte | ||||||
|  | 	binary.BigEndian.PutUint16(b[:], uint16(typ)) | ||||||
|  | 	return b[:] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func PackKey(typ DataTyp, data []byte) []byte { | ||||||
|  | 	v := typToBytes(typ) | ||||||
|  | 	return append(v, data...) | ||||||
|  | } | ||||||
| @ -1,11 +0,0 @@ | |||||||
| package utils |  | ||||||
| 
 |  | ||||||
| var isoCodes = map[string]bool{ |  | ||||||
| 	"eng":     true, // English
 |  | ||||||
| 	"swa":     true, // Swahili
 |  | ||||||
| 	"default": true, // Default language: English
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func IsValidISO639(code string) bool { |  | ||||||
| 	return isoCodes[code] |  | ||||||
| } |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| package utils |  | ||||||
| 
 |  | ||||||
| func ConstructName(firstName, familyName, defaultValue string) string { |  | ||||||
| 	name := defaultValue |  | ||||||
| 	if familyName != defaultValue { |  | ||||||
| 		if firstName != defaultValue { |  | ||||||
| 			name = firstName + " " + familyName |  | ||||||
| 		} else { |  | ||||||
| 			name = familyName |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		if firstName != defaultValue { |  | ||||||
| 			name = firstName |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return name |  | ||||||
| } |  | ||||||
| @ -1,4 +1,4 @@ | |||||||
| package common | package utils | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| @ -16,19 +16,17 @@ type UserDataStore struct { | |||||||
| 	db.Db | 	db.Db | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ReadEntry retrieves an entry to the userdata store.
 | // ReadEntry retrieves an entry from the store based on the provided parameters.
 | ||||||
| func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { | func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { | ||||||
| 	store.SetPrefix(db.DATATYPE_USERDATA) | 	store.SetPrefix(db.DATATYPE_USERDATA) | ||||||
| 	store.SetSession(sessionId) | 	store.SetSession(sessionId) | ||||||
| 	k := ToBytes(typ) | 	k := PackKey(typ, []byte(sessionId)) | ||||||
| 	return store.Get(ctx, k) | 	return store.Get(ctx, k) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // WriteEntry adds an entry to the userdata store.
 |  | ||||||
| // BUG: this uses sessionId twice
 |  | ||||||
| func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { | func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { | ||||||
| 	store.SetPrefix(db.DATATYPE_USERDATA) | 	store.SetPrefix(db.DATATYPE_USERDATA) | ||||||
| 	store.SetSession(sessionId) | 	store.SetSession(sessionId) | ||||||
| 	k := ToBytes(typ) | 	k := PackKey(typ, []byte(sessionId)) | ||||||
| 	return store.Put(ctx, k, value) | 	return store.Put(ctx, k, value) | ||||||
| } | } | ||||||
| @ -1,443 +0,0 @@ | |||||||
| { |  | ||||||
|     "groups": [ |  | ||||||
|         { |  | ||||||
|             "name": "my_account_change_pin", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "5", |  | ||||||
|                     "expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Enter your old PIN\n\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Enter a new four number PIN:\n\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Confirm your new PIN:\n\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Your PIN change request has been successful\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_language_change", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "2", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1235", |  | ||||||
|                     "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Select language:\n1:English\n2:Kiswahili" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Your language change request was successful.\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_check_my_balance", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1235", |  | ||||||
|                     "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Balance: {balance}\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_check_community_balance", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "2", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1235", |  | ||||||
|                     "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "{balance}\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_edit_all_account_details_starting_from_firstname", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Enter your first names:\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "foo", |  | ||||||
|                     "expectedContent": "Enter family name:\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "bar", |  | ||||||
|                     "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Enter your year of birth\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1940", |  | ||||||
|                     "expectedContent": "Enter your location:\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "Kilifi", |  | ||||||
|                     "expectedContent": "Enter the services or goods you offer: \n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "Bananas", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_edit_familyname_when_all_account__details_have_been_set", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "2", |  | ||||||
|                     "expectedContent": "Enter family name:\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "bar", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_edit_gender_when_all_account__details_have_been_set", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_edit_yob_when_all_account__details_have_been_set", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "4", |  | ||||||
|                     "expectedContent": "Enter your year of birth\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1945", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_edit_location_when_all_account_details_have_been_set", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "5", |  | ||||||
|                     "expectedContent": "Enter your location:\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "Kilifi", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_edit_offerings_when_all_account__details_have_been_set", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "6", |  | ||||||
|                     "expectedContent": "Enter the services or goods you offer: \n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "Bananas", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "menu_my_account_view_profile", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "7", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @ -1,386 +0,0 @@ | |||||||
| package menutraversaltest |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"context" |  | ||||||
| 	"flag" |  | ||||||
| 	"log" |  | ||||||
| 	"math/rand" |  | ||||||
| 	"regexp" |  | ||||||
| 	"testing" |  | ||||||
| 
 |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil" |  | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/driver" |  | ||||||
| 	"github.com/gofrs/uuid" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	testData  = driver.ReadData() |  | ||||||
| 	sessionID string |  | ||||||
| 	src       = rand.NewSource(42) |  | ||||||
| 	g         = rand.New(src) |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests") |  | ||||||
| var database = flag.String("db", "gdbm", "Specify the database (gdbm or postgres)") |  | ||||||
| var connStr = flag.String("conn", ".test_state", "connection string") |  | ||||||
| var dbSchema = flag.String("schema", "test", "Specify the database schema (default test)") |  | ||||||
| 
 |  | ||||||
| func GenerateSessionId() string { |  | ||||||
| 	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) |  | ||||||
| 	v, err := uu.NewV4() |  | ||||||
| 	if err != nil { |  | ||||||
| 		panic(err) |  | ||||||
| 	} |  | ||||||
| 	return v.String() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Extract the public key from the engine response
 |  | ||||||
| func extractPublicKey(response []byte) string { |  | ||||||
| 	// Regex pattern to match the public key starting with 0x and 40 characters
 |  | ||||||
| 	re := regexp.MustCompile(`0x[a-fA-F0-9]{40}`) |  | ||||||
| 	match := re.Find(response) |  | ||||||
| 	if match != nil { |  | ||||||
| 		return string(match) |  | ||||||
| 	} |  | ||||||
| 	return "" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Extracts the balance value from the engine response.
 |  | ||||||
| func extractBalance(response []byte) string { |  | ||||||
| 	// Regex to match "Balance: <amount> <symbol>" followed by a newline
 |  | ||||||
| 	re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`) |  | ||||||
| 	match := re.FindSubmatch(response) |  | ||||||
| 	if match != nil { |  | ||||||
| 		return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
 |  | ||||||
| 	} |  | ||||||
| 	return "" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Extracts the Maximum amount value from the engine response.
 |  | ||||||
| func extractMaxAmount(response []byte) string { |  | ||||||
| 	// Regex to match "Maximum amount: <amount>" followed by a newline
 |  | ||||||
| 	re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`) |  | ||||||
| 	match := re.FindSubmatch(response) |  | ||||||
| 	if match != nil { |  | ||||||
| 		return string(match[1]) // "<amount>"
 |  | ||||||
| 	} |  | ||||||
| 	return "" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Extracts the send amount value from the engine response.
 |  | ||||||
| func extractSendAmount(response []byte) string { |  | ||||||
| 	// Regex to match the pattern "will receive X.XX SYM from"
 |  | ||||||
| 	re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`) |  | ||||||
| 	match := re.FindSubmatch(response) |  | ||||||
| 	if match != nil { |  | ||||||
| 		return string(match[1]) // Returns "X.XX SYM"
 |  | ||||||
| 	} |  | ||||||
| 	return "" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMain(m *testing.M) { |  | ||||||
| 	// Parse the flags
 |  | ||||||
| 	flag.Parse() |  | ||||||
| 	sessionID = GenerateSessionId() |  | ||||||
| 	// set the db
 |  | ||||||
| 	testutil.SetDatabase(*database, *connStr, *dbSchema) |  | ||||||
| 
 |  | ||||||
| 	// Cleanup the db after tests
 |  | ||||||
| 	defer testutil.CleanDatabase() |  | ||||||
| 
 |  | ||||||
| 	m.Run() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestAccountCreationSuccessful(t *testing.T) { |  | ||||||
| 	en, fn, eventChannel := testutil.TestEngine(sessionID) |  | ||||||
| 	defer fn() |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessions := testData |  | ||||||
| 	for _, session := range sessions { |  | ||||||
| 		groups := driver.FilterGroupsByName(session.Groups, "account_creation_successful") |  | ||||||
| 		for _, group := range groups { |  | ||||||
| 			for _, step := range group.Steps { |  | ||||||
| 				cont, err := en.Exec(ctx, []byte(step.Input)) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) |  | ||||||
| 				} |  | ||||||
| 				if !cont { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 				w := bytes.NewBuffer(nil) |  | ||||||
| 				_, err = en.Flush(ctx, w) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) |  | ||||||
| 				} |  | ||||||
| 				b := w.Bytes() |  | ||||||
| 				match, err := step.MatchesExpectedContent(b) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) |  | ||||||
| 				} |  | ||||||
| 				if !match { |  | ||||||
| 					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	<-eventChannel |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestAccountRegistrationRejectTerms(t *testing.T) { |  | ||||||
| 	// Generate a new UUID for this edge case test
 |  | ||||||
| 	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) |  | ||||||
| 	v, err := uu.NewV4() |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fail() |  | ||||||
| 	} |  | ||||||
| 	edgeCaseSessionID := v.String() |  | ||||||
| 	en, fn, _ := testutil.TestEngine(edgeCaseSessionID) |  | ||||||
| 	defer fn() |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessions := testData |  | ||||||
| 	for _, session := range sessions { |  | ||||||
| 		groups := driver.FilterGroupsByName(session.Groups, "account_creation_reject_terms") |  | ||||||
| 		for _, group := range groups { |  | ||||||
| 			for _, step := range group.Steps { |  | ||||||
| 				cont, err := en.Exec(ctx, []byte(step.Input)) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 				if !cont { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 				w := bytes.NewBuffer(nil) |  | ||||||
| 				if _, err := en.Flush(ctx, w); err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				b := w.Bytes() |  | ||||||
| 				match, err := step.MatchesExpectedContent(b) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) |  | ||||||
| 				} |  | ||||||
| 				if !match { |  | ||||||
| 					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMainMenuHelp(t *testing.T) { |  | ||||||
| 	en, fn, _ := testutil.TestEngine(sessionID) |  | ||||||
| 	defer fn() |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessions := testData |  | ||||||
| 	for _, session := range sessions { |  | ||||||
| 		groups := driver.FilterGroupsByName(session.Groups, "main_menu_help") |  | ||||||
| 		for _, group := range groups { |  | ||||||
| 			for _, step := range group.Steps { |  | ||||||
| 				cont, err := en.Exec(ctx, []byte(step.Input)) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 				if !cont { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 				w := bytes.NewBuffer(nil) |  | ||||||
| 				if _, err := en.Flush(ctx, w); err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				b := w.Bytes() |  | ||||||
| 				balance := extractBalance(b) |  | ||||||
| 
 |  | ||||||
| 				expectedContent := []byte(step.ExpectedContent) |  | ||||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) |  | ||||||
| 
 |  | ||||||
| 				step.ExpectedContent = string(expectedContent) |  | ||||||
| 				match, err := step.MatchesExpectedContent(b) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) |  | ||||||
| 				} |  | ||||||
| 				if !match { |  | ||||||
| 					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMainMenuQuit(t *testing.T) { |  | ||||||
| 	en, fn, _ := testutil.TestEngine(sessionID) |  | ||||||
| 	defer fn() |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessions := testData |  | ||||||
| 	for _, session := range sessions { |  | ||||||
| 		groups := driver.FilterGroupsByName(session.Groups, "main_menu_quit") |  | ||||||
| 		for _, group := range groups { |  | ||||||
| 			for _, step := range group.Steps { |  | ||||||
| 				cont, err := en.Exec(ctx, []byte(step.Input)) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 				if !cont { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 				w := bytes.NewBuffer(nil) |  | ||||||
| 				if _, err := en.Flush(ctx, w); err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				b := w.Bytes() |  | ||||||
| 				balance := extractBalance(b) |  | ||||||
| 
 |  | ||||||
| 				expectedContent := []byte(step.ExpectedContent) |  | ||||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) |  | ||||||
| 
 |  | ||||||
| 				step.ExpectedContent = string(expectedContent) |  | ||||||
| 				match, err := step.MatchesExpectedContent(b) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) |  | ||||||
| 				} |  | ||||||
| 				if !match { |  | ||||||
| 					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMyAccount_MyAddress(t *testing.T) { |  | ||||||
| 	en, fn, _ := testutil.TestEngine(sessionID) |  | ||||||
| 	defer fn() |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessions := testData |  | ||||||
| 	for _, session := range sessions { |  | ||||||
| 		groups := driver.FilterGroupsByName(session.Groups, "menu_my_account_my_address") |  | ||||||
| 		for _, group := range groups { |  | ||||||
| 			for index, step := range group.Steps { |  | ||||||
| 				t.Logf("step %v with input %v", index, step.Input) |  | ||||||
| 				cont, err := en.Exec(ctx, []byte(step.Input)) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Errorf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 				if !cont { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 				w := bytes.NewBuffer(nil) |  | ||||||
| 				if _, err := en.Flush(ctx, w); err != nil { |  | ||||||
| 					t.Errorf("Test case '%s' failed during Flush: %v", group.Name, err) |  | ||||||
| 				} |  | ||||||
| 				b := w.Bytes() |  | ||||||
| 
 |  | ||||||
| 				balance := extractBalance(b) |  | ||||||
| 				publicKey := extractPublicKey(b) |  | ||||||
| 
 |  | ||||||
| 				expectedContent := []byte(step.ExpectedContent) |  | ||||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) |  | ||||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1) |  | ||||||
| 
 |  | ||||||
| 				step.ExpectedContent = string(expectedContent) |  | ||||||
| 				match, err := step.MatchesExpectedContent(b) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) |  | ||||||
| 				} |  | ||||||
| 				if !match { |  | ||||||
| 					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expectedContent, b) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestMainMenuSend(t *testing.T) { |  | ||||||
| 	en, fn, _ := testutil.TestEngine(sessionID) |  | ||||||
| 	defer fn() |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	sessions := testData |  | ||||||
| 	for _, session := range sessions { |  | ||||||
| 		groups := driver.FilterGroupsByName(session.Groups, "send_with_invite") |  | ||||||
| 		for _, group := range groups { |  | ||||||
| 			for index, step := range group.Steps { |  | ||||||
| 				t.Logf("step %v with input %v", index, step.Input) |  | ||||||
| 				cont, err := en.Exec(ctx, []byte(step.Input)) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 				if !cont { |  | ||||||
| 					break |  | ||||||
| 				} |  | ||||||
| 				w := bytes.NewBuffer(nil) |  | ||||||
| 				if _, err := en.Flush(ctx, w); err != nil { |  | ||||||
| 					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				b := w.Bytes() |  | ||||||
| 				balance := extractBalance(b) |  | ||||||
| 				max_amount := extractMaxAmount(b) |  | ||||||
| 				send_amount := extractSendAmount(b) |  | ||||||
| 
 |  | ||||||
| 				expectedContent := []byte(step.ExpectedContent) |  | ||||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) |  | ||||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1) |  | ||||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1) |  | ||||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1) |  | ||||||
| 
 |  | ||||||
| 				step.ExpectedContent = string(expectedContent) |  | ||||||
| 				match, err := step.MatchesExpectedContent(b) |  | ||||||
| 				if err != nil { |  | ||||||
| 					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) |  | ||||||
| 				} |  | ||||||
| 				if !match { |  | ||||||
| 					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestGroups(t *testing.T) { |  | ||||||
| 	groups, err := driver.LoadTestGroups(*groupTestFile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatalf("Failed to load test groups: %v", err) |  | ||||||
| 	} |  | ||||||
| 	en, fn, _ := testutil.TestEngine(sessionID) |  | ||||||
| 	defer fn() |  | ||||||
| 	ctx := context.Background() |  | ||||||
| 	// Create test cases from loaded groups
 |  | ||||||
| 	tests := driver.CreateTestCases(groups) |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.Name, func(t *testing.T) { |  | ||||||
| 			cont, err := en.Exec(ctx, []byte(tt.Input)) |  | ||||||
| 			if err != nil { |  | ||||||
| 				t.Errorf("Test case '%s' failed at input '%s': %v", tt.Name, tt.Input, err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			if !cont { |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			w := bytes.NewBuffer(nil) |  | ||||||
| 			if _, err := en.Flush(ctx, w); err != nil { |  | ||||||
| 				t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err) |  | ||||||
| 			} |  | ||||||
| 			b := w.Bytes() |  | ||||||
| 			balance := extractBalance(b) |  | ||||||
| 
 |  | ||||||
| 			expectedContent := []byte(tt.ExpectedContent) |  | ||||||
| 			expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) |  | ||||||
| 
 |  | ||||||
| 			tt.ExpectedContent = string(expectedContent) |  | ||||||
| 
 |  | ||||||
| 			match, err := tt.MatchesExpectedContent(b) |  | ||||||
| 			if err != nil { |  | ||||||
| 				t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err) |  | ||||||
| 			} |  | ||||||
| 			if !match { |  | ||||||
| 				t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @ -1,68 +0,0 @@ | |||||||
| { |  | ||||||
|     "groups": [ |  | ||||||
|         {  |  | ||||||
|                 "name": "menu_my_account_edit_all_account_details_starting_from_family_name", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "3", |  | ||||||
|                         "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "2", |  | ||||||
|                         "expectedContent": "Enter family name:\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "bar", |  | ||||||
|                         "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent":  "Enter your year of birth\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1940", |  | ||||||
|                         "expectedContent": "Enter your location:\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Kilifi", |  | ||||||
|                         "expectedContent": "Enter the services or goods you offer: \n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Bananas", |  | ||||||
|                         "expectedContent": "Please enter your PIN:" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1234", |  | ||||||
|                         "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
|        |  | ||||||
|          |  | ||||||
|         |  | ||||||
|          |  | ||||||
|      |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|      |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|      |  | ||||||
| @ -1,61 +0,0 @@ | |||||||
| { |  | ||||||
|     "groups": [ |  | ||||||
|         {  |  | ||||||
|             "name": "menu_my_account_edit_all_account_details_starting_from_firstname", |  | ||||||
|             "steps": [ |  | ||||||
|                 { |  | ||||||
|                     "input": "", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "3", |  | ||||||
|                     "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent": "Enter your first names:\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "foo", |  | ||||||
|                     "expectedContent": "Enter family name:\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "bar", |  | ||||||
|                     "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1", |  | ||||||
|                     "expectedContent":  "Enter your year of birth\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1940", |  | ||||||
|                     "expectedContent": "Enter your location:\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "Kilifi", |  | ||||||
|                     "expectedContent": "Enter the services or goods you offer: \n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "Bananas", |  | ||||||
|                     "expectedContent": "Please enter your PIN:" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "1234", |  | ||||||
|                     "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "input": "0", |  | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @ -1,55 +0,0 @@ | |||||||
| { |  | ||||||
|     "groups": [ |  | ||||||
|         {  |  | ||||||
|                 "name": "menu_my_account_edit_all_account_details_starting_from_gender", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "3", |  | ||||||
|                         "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "3", |  | ||||||
|                         "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent":  "Enter your year of birth\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1940", |  | ||||||
|                         "expectedContent": "Enter your location:\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Kilifi", |  | ||||||
|                         "expectedContent": "Enter the services or goods you offer: \n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Bananas", |  | ||||||
|                         "expectedContent": "Please enter your PIN:" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1234", |  | ||||||
|                         "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
|        |  | ||||||
|        |  | ||||||
| @ -1,46 +0,0 @@ | |||||||
| { |  | ||||||
|     "groups": [ |  | ||||||
|         {  |  | ||||||
|                 "name": "menu_my_account_edit_all_account_details_starting_from_location", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "3", |  | ||||||
|                         "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     },   |  | ||||||
|                     { |  | ||||||
|                         "input": "5", |  | ||||||
|                         "expectedContent": "Enter your location:\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Kilifi", |  | ||||||
|                         "expectedContent": "Enter the services or goods you offer: \n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Bananas", |  | ||||||
|                         "expectedContent": "Please enter your PIN:" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1234", |  | ||||||
|                         "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
|       |  | ||||||
| @ -1,42 +0,0 @@ | |||||||
| { |  | ||||||
|     "groups": [ |  | ||||||
|         {  |  | ||||||
|                 "name": "menu_my_account_edit_all_account_details_starting_from_offerings", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "3", |  | ||||||
|                         "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "6", |  | ||||||
|                         "expectedContent": "Enter the services or goods you offer: \n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Bananas", |  | ||||||
|                         "expectedContent": "Please enter your PIN:" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1234", |  | ||||||
|                         "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
|       |  | ||||||
| @ -1,50 +0,0 @@ | |||||||
| { |  | ||||||
|     "groups": [ |  | ||||||
|         {  |  | ||||||
|                 "name": "menu_my_account_edit_all_account_details_starting_from_yob", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "3", |  | ||||||
|                         "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "4", |  | ||||||
|                         "expectedContent":  "Enter your year of birth\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1940", |  | ||||||
|                         "expectedContent": "Enter your location:\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Kilifi", |  | ||||||
|                         "expectedContent": "Enter the services or goods you offer: \n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Bananas", |  | ||||||
|                         "expectedContent": "Please enter your PIN:" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1234", |  | ||||||
|                         "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
|       |  | ||||||
| @ -1,70 +0,0 @@ | |||||||
| { |  | ||||||
|     "groups": [ |  | ||||||
|         {  |  | ||||||
|                 "name": "menu_my_account_edit_familyname_when_adjacent_profile_information_set", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "3", |  | ||||||
|                         "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "3", |  | ||||||
|                         "expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent":  "Enter your year of birth\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1940", |  | ||||||
|                         "expectedContent": "Enter your location:\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Kilifi", |  | ||||||
|                         "expectedContent": "Enter the services or goods you offer: \n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "Bananas", |  | ||||||
|                         "expectedContent": "Please enter your PIN:" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1234", |  | ||||||
|                         "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "2", |  | ||||||
|                         "expectedContent": "Enter family name:\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "foo2", |  | ||||||
|                         "expectedContent": "Please enter your PIN:" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1234", |  | ||||||
|                         "expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                        "input": "0", |  | ||||||
|                        "expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
|      |  | ||||||
| @ -1,133 +0,0 @@ | |||||||
| [ |  | ||||||
|     { |  | ||||||
|         "name": "session one", |  | ||||||
|         "groups": [ |  | ||||||
|             { |  | ||||||
|                 "name": "account_creation_successful", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "Please enter a new four number PIN for your account:\n0:Exit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1234", |  | ||||||
|                         "expectedContent": "Enter your four number PIN again:" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1111", |  | ||||||
|                         "expectedContent": "The PIN is not a match. Try again\n1:Retry\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "Enter your four number PIN again:" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1234", |  | ||||||
|                         "expectedContent": "Your account is being created...Thank you for using Sarafu. Goodbye!" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 "name": "account_creation_reject_terms", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "2", |  | ||||||
|                         "expectedContent": "Thank you for using Sarafu. Goodbye!" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 "name": "send_with_invite", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0@0", |  | ||||||
|                         "expectedContent": "0@0 is invalid, please try again:\n1:Retry\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "1", |  | ||||||
|                         "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0712345678", |  | ||||||
|                         "expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "2", |  | ||||||
|                         "expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later." |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 "name": "main_menu_help", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "4", |  | ||||||
|                         "expectedContent": "For more help,please call: 0757628885" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 "name": "main_menu_quit", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "9", |  | ||||||
|                         "expectedContent": "Thank you for using Sarafu. Goodbye!" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 "name": "menu_my_account_my_address", |  | ||||||
|                 "steps": [ |  | ||||||
|                     { |  | ||||||
|                         "input": "", |  | ||||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "3", |  | ||||||
|                         "expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "6", |  | ||||||
|                         "expectedContent": "Address: {public_key}\n0:Back\n9:Quit" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "9", |  | ||||||
|                         "expectedContent": "Thank you for using Sarafu. Goodbye!" |  | ||||||
|                     } |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|         ] |  | ||||||
|     } |  | ||||||
| ] |  | ||||||
| @ -1,6 +0,0 @@ | |||||||
| package models |  | ||||||
| 
 |  | ||||||
| type AccountResult struct { |  | ||||||
| 	PublicKey  string `json:"publicKey"` |  | ||||||
| 	TrackingId string `json:"trackingId"` |  | ||||||
| } |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user