forked from grassrootseconomics/visedriver
		
	Compare commits
	
		
			196 Commits
		
	
	
		
			lash/purif
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 770fef68b9 | |||
| dd2751839d | |||
| 5a9c822075 | |||
|  | c8c0daac24 | ||
|  | 1bcffe2d23 | ||
| bf10e5357c | |||
|  | d91c96f541 | ||
|  | ec9032a42e | ||
|  | 6619afe62b | ||
|  | 1eb0b15552 | ||
|  | ef803e0ee2 | ||
|  | 03d19283f6 | ||
|  | 15ce29a1a4 | ||
|  | 6749c632b0 | ||
|  | 8530c45074 | ||
|  | d5e636fbd6 | ||
|  | f7d31e4e81 | ||
|  | 90ecec1798 | ||
|  | 874edb3da6 | ||
|  | 60ff1b0ab3 | ||
|  | 9b3dad579b | ||
|  | 348fff8936 | ||
|  | c5bb1c80a5 | ||
|  | b8a377befb | ||
|  | c9b92191f3 | ||
|  | ddd8d7cac0 | ||
|  | 37973a6c9b | ||
|  | 975720919c | ||
|  | c0534ede1b | ||
|  | 24e729d275 | ||
|  | ae6e2a99c5 | ||
|  | 7ca3974371 | ||
|  | adbfab3964 | ||
|  | 5228aef088 | ||
|  | 5bf0a0e858 | ||
|  | f13dab9a45 | ||
|  | 3f8e08151a | ||
|  | 9e4c65c8b4 | ||
|  | 611c5a8dfc | ||
|  | dcf777bf08 | ||
|  | ec4ad6e44b | ||
|  | 5b312f9569 | ||
|  | 2d89db3a37 | ||
|  | 6d8992fe38 | ||
|  | 5cffbe5cd8 | ||
|  | b28a047777 | ||
|  | 2ea51d88d8 | ||
|  | 2e0e854c4b | ||
|  | c93a07832d | ||
|  | 46bf21b7b8 | ||
|  | dff662663d | ||
|  | 977d14c529 | ||
|  | fd6e5caf53 | ||
|  | 840c22ca89 | ||
|  | f939a20543 | ||
|  | 8387644019 | ||
|  | c535938dbc | ||
|  | b60d2648ea | ||
|  | 85ede15613 | ||
| b11f11b5fa | |||
| 3d35a5de78 | |||
| a19ace85f8 | |||
|  | a7a8a482ab | ||
|  | d8e7c443b5 | ||
|  | 5216a9383e | ||
|  | 6cd639fd19 | ||
|  | a7ca280964 | ||
| 8f5ed0cd4f | |||
|  | f1664a43a8 | ||
| c29abfe21e | |||
| 9a6d8e5158 | |||
|  | 9ca5091692 | ||
| db431c750e | |||
| 2b9c6d641e | |||
|  | b9712098ef | ||
|  | bcf1965a6c | ||
| 3747f87a7c | |||
| 1f0568df32 | |||
|  | 24c513d4f0 | ||
| b3fd6f5c1a | |||
| 73eb765408 | |||
| f660f6c19a | |||
| 3fccfaab61 | |||
|  | b50a51df9b | ||
|  | df8c9aab0c | ||
|  | ddefdd7fb3 | ||
| 5734011f96 | |||
|  | 379d98ccd5 | ||
| f40e11c267 | |||
| b698f08136 | |||
| 4d7589ad95 | |||
| efdb52bccd | |||
| 2ff9fed3c5 | |||
| 477b4cf8f6 | |||
| ed6651697a | |||
| c359d99075 | |||
| 8d477356f3 | |||
| 7f3294a8a2 | |||
| 4b5f08e25e | |||
| ea9cab930e | |||
| a37f6e6da3 | |||
| f59c3a53ef | |||
| 81c3378ea6 | |||
| 46a6d2bc6e | |||
|  | 721f80d0f2 | ||
| f49e54a562 | |||
|  | 5081b6d4ce | ||
| 4d72ae0313 | |||
| 4fe64a7747 | |||
| 3004698d5b | |||
| 50c7ff1046 | |||
| 07b061a68b | |||
| 6339f0c2e5 | |||
|  | 1fa830f286 | ||
| 64fba91670 | |||
| c15958a1ad | |||
| ee442daefa | |||
| 656052dc74 | |||
| 6c5873da6f | |||
| 11d30583a4 | |||
| f83f539046 | |||
| 562bd4fa24 | |||
| 90df0eefc3 | |||
| b37f2a0a11 | |||
| 68e4c9af03 | |||
| c12e867ac3 | |||
| 79de0a9092 | |||
| 3ee15497a5 | |||
|  | e09e324a50 | ||
|  | 599815c343 | ||
|  | 462c0d7677 | ||
| 80b96e9bf6 | |||
| b5561decd1 | |||
|  | 02823fd64e | ||
|  | cd575c2edb | ||
| f3d4f35718 | |||
| 52787bdb4d | |||
|  | f8d8f265f1 | ||
|  | 9371b52f3e | ||
|  | 563000ec15 | ||
| 824d39908b | |||
|  | 52fd1eced2 | ||
| a312ea5b84 | |||
|  | 5c7a535288 | ||
| 4836162f40 | |||
|  | cc2f7b41df | ||
|  | d39740a09a | ||
|  | 2024cc96e2 | ||
|  | d2d878d5d7 | ||
| c995143543 | |||
| 44570e20ef | |||
| 362eb209ef | |||
| c69d3896f1 | |||
| 974af6b2a7 | |||
|  | bb4037e73f | ||
|  | 739fd90dfd | ||
|  | 6789c4f550 | ||
|  | 437f73827d | ||
|  | f0a4a0df61 | ||
|  | bd604219b8 | ||
|  | 51b6fc0dde | ||
|  | cc9760125a | ||
|  | 3a9f3fa373 | ||
|  | 89c21847b9 | ||
|  | 450dfa02cc | ||
|  | f61e65f4fe | ||
|  | a4d6cef9c0 | ||
|  | 2992f7ae8e | ||
|  | dc61d05584 | ||
|  | 83857026d3 | ||
|  | 349051b5ef | ||
| 47b5ff0435 | |||
|  | e92e498726 | ||
|  | c3cbe1cd92 | ||
|  | 418080d093 | ||
|  | 2e30739ec9 | ||
|  | dc1674ec55 | ||
|  | daec816a3e | ||
|  | ac0c43cb43 | ||
|  | 9013cc3618 | ||
|  | 056d056613 | ||
|  | e581ec4771 | ||
|  | e16b7445e8 | ||
|  | 1b12f0ba5f | ||
|  | c1e0617bb3 | ||
|  | 6723884103 | ||
|  | b888af446d | ||
|  | 43b2c3b78d | ||
|  | d67853f6d9 | ||
|  | 06230dc557 | ||
|  | 6ee2c88fe2 | ||
|  | bb1a846cb3 | ||
|  | 967e53d83b | ||
|  | d246cdee51 | ||
|  | d518a76536 | ||
|  | 6f65c33be4 | 
| @ -1,13 +0,0 @@ | ||||
| /** | ||||
| !/cmd/africastalking | ||||
| !/common | ||||
| !/config | ||||
| !/initializers | ||||
| !/internal | ||||
| !/models | ||||
| !/remote | ||||
| !/services | ||||
| !/LICENSE | ||||
| !/README.md | ||||
| !/go.* | ||||
| !/.env.example | ||||
							
								
								
									
										14
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								.env.example
									
									
									
									
									
								
							| @ -6,15 +6,15 @@ HOST=127.0.0.1 | ||||
| AT_ENDPOINT=/ussd/africastalking | ||||
| 
 | ||||
| #PostgreSQL | ||||
| DB_HOST=localhost | ||||
| DB_USER=postgres | ||||
| DB_PASSWORD=strongpass | ||||
| DB_NAME=urdt_ussd | ||||
| DB_PORT=5432 | ||||
| DB_SSLMODE=disable | ||||
| DB_TIMEZONE=Africa/Nairobi | ||||
| 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 }} | ||||
							
								
								
									
										41
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,41 +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 | ||||
| 
 | ||||
| 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/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 | ||||
| 
 | ||||
| CMD ["./ussd-africastalking"] | ||||
							
								
								
									
										92
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										92
									
								
								README.md
									
									
									
									
									
								
							| @ -1,92 +0,0 @@ | ||||
| # URDT USSD service | ||||
| 
 | ||||
| This is a USSD service built using the [go-vise](https://github.com/nolash/go-vise) engine. | ||||
| 
 | ||||
| ## Prerequisites | ||||
| ### 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 | ||||
| 
 | ||||
| [AGPL-3.0](LICENSE). | ||||
| @ -1,154 +0,0 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"syscall" | ||||
| 
 | ||||
| 	"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/http/at" | ||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http/at" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg          = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id") | ||||
| 	scriptDir     = path.Join("services", "registration") | ||||
| 	build         = "dev" | ||||
| 	menuSeparator = ": " | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	initializers.LoadEnvVariables() | ||||
| } | ||||
| func main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var dbDir string | ||||
| 	var resourceDir string | ||||
| 	var size uint | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	var host string | ||||
| 	var port uint | ||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | ||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | ||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 	pfp := path.Join(scriptDir, "pp.csv") | ||||
| 
 | ||||
| 	cfg := engine.Config{ | ||||
| 		Root:          "root", | ||||
| 		OutputSize:    uint32(size), | ||||
| 		FlagCount:     uint32(128), | ||||
| 		MenuSeparator: menuSeparator, | ||||
| 	} | ||||
| 
 | ||||
| 	if engineDebug { | ||||
| 		cfg.EngineDebug = true | ||||
| 	} | ||||
| 
 | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	err = menuStorageService.EnsureDbDir() | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	userdataStore, err := menuStorageService.GetUserdataDb(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer userdataStore.Close() | ||||
| 
 | ||||
| 	dbResource, ok := rs.(*resource.DbResource) | ||||
| 	if !ok { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	lhs.SetDataStore(&userdataStore) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	accountService := remote.AccountService{} | ||||
| 	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 { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer stateStore.Close() | ||||
| 
 | ||||
| 	rp := &at.ATRequestParser{ | ||||
| 		Context: ctx, | ||||
| 	} | ||||
| 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | ||||
| 	sh := httpserver.NewATSessionHandler(bsh) | ||||
| 
 | ||||
| 	mux := http.NewServeMux() | ||||
| 	mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) | ||||
| 
 | ||||
| 	s := &http.Server{ | ||||
| 		Addr:    fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), | ||||
| 		Handler: mux, | ||||
| 	} | ||||
| 	s.RegisterOnShutdown(sh.Shutdown) | ||||
| 
 | ||||
| 	cint := make(chan os.Signal) | ||||
| 	cterm := make(chan os.Signal) | ||||
| 	signal.Notify(cint, os.Interrupt, syscall.SIGINT) | ||||
| 	signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) | ||||
| 	go func() { | ||||
| 		select { | ||||
| 		case _ = <-cint: | ||||
| 		case _ = <-cterm: | ||||
| 		} | ||||
| 		s.Shutdown(ctx) | ||||
| 	}() | ||||
| 	err = s.ListenAndServe() | ||||
| 	if err != nil { | ||||
| 		logg.Infof("Server closed with error", "err", err) | ||||
| 	} | ||||
| } | ||||
| @ -1,176 +0,0 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"path" | ||||
| 	"syscall" | ||||
| 
 | ||||
| 	"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/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| 	menuSeparator = ": " | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	initializers.LoadEnvVariables() | ||||
| } | ||||
| 
 | ||||
| type asyncRequestParser struct { | ||||
| 	sessionId string | ||||
| 	input     []byte | ||||
| } | ||||
| 
 | ||||
| func (p *asyncRequestParser) GetSessionId(r any) (string, error) { | ||||
| 	return p.sessionId, nil | ||||
| } | ||||
| 
 | ||||
| func (p *asyncRequestParser) GetInput(r any) ([]byte, error) { | ||||
| 	return p.input, nil | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var sessionId string | ||||
| 	var dbDir string | ||||
| 	var resourceDir string | ||||
| 	var size uint | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	var host string | ||||
| 	var port uint | ||||
| 	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(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | ||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | ||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 	pfp := path.Join(scriptDir, "pp.csv") | ||||
| 
 | ||||
| 	cfg := engine.Config{ | ||||
| 		Root:          "root", | ||||
| 		OutputSize:    uint32(size), | ||||
| 		FlagCount:     uint32(128), | ||||
| 		MenuSeparator: menuSeparator, | ||||
| 	} | ||||
| 
 | ||||
| 	if engineDebug { | ||||
| 		cfg.EngineDebug = true | ||||
| 	} | ||||
| 
 | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	err = menuStorageService.EnsureDbDir() | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	userdataStore, err := menuStorageService.GetUserdataDb(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer userdataStore.Close() | ||||
| 
 | ||||
| 	dbResource, ok := rs.(*resource.DbResource) | ||||
| 	if !ok { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	lhs.SetDataStore(&userdataStore) | ||||
| 	accountService := remote.AccountService{} | ||||
| 
 | ||||
| 	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 { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer stateStore.Close() | ||||
| 
 | ||||
| 	rp := &asyncRequestParser{ | ||||
| 		sessionId: sessionId, | ||||
| 	} | ||||
| 	sh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | ||||
| 	cfg.SessionId = sessionId | ||||
| 	rqs := handlers.RequestSession{ | ||||
| 		Ctx:    ctx, | ||||
| 		Writer: os.Stdout, | ||||
| 		Config: cfg, | ||||
| 	} | ||||
| 
 | ||||
| 	cint := make(chan os.Signal) | ||||
| 	cterm := make(chan os.Signal) | ||||
| 	signal.Notify(cint, os.Interrupt, syscall.SIGINT) | ||||
| 	signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) | ||||
| 	go func() { | ||||
| 		select { | ||||
| 		case _ = <-cint: | ||||
| 		case _ = <-cterm: | ||||
| 		} | ||||
| 		sh.Shutdown() | ||||
| 	}() | ||||
| 
 | ||||
| 	for true { | ||||
| 		rqs, err = sh.Process(rqs) | ||||
| 		if err != nil { | ||||
| 			logg.ErrorCtxf(ctx, "error in process: %v", "err", err) | ||||
| 			fmt.Errorf("error in process: %v", err) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		rqs, err = sh.Output(rqs) | ||||
| 		if err != nil { | ||||
| 			logg.ErrorCtxf(ctx, "error in output: %v", "err", err) | ||||
| 			fmt.Errorf("error in output: %v", err) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		rqs, err = sh.Reset(rqs) | ||||
| 		if err != nil { | ||||
| 			logg.ErrorCtxf(ctx, "error in reset: %v", "err", err) | ||||
| 			fmt.Errorf("error in reset: %v", err) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		fmt.Println("") | ||||
| 		_, err = fmt.Scanln(&rqs.Input) | ||||
| 		if err != nil { | ||||
| 			logg.ErrorCtxf(ctx, "error in input", "err", err) | ||||
| 			fmt.Errorf("error in input: %v", err) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										143
									
								
								cmd/http/main.go
									
									
									
									
									
								
							
							
						
						
									
										143
									
								
								cmd/http/main.go
									
									
									
									
									
								
							| @ -1,143 +0,0 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"syscall" | ||||
| 
 | ||||
| 	"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" | ||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| 	menuSeparator = ": " | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	initializers.LoadEnvVariables() | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var dbDir string | ||||
| 	var resourceDir string | ||||
| 	var size uint | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	var host string | ||||
| 	var port uint | ||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | ||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | ||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 	pfp := path.Join(scriptDir, "pp.csv") | ||||
| 
 | ||||
| 	cfg := engine.Config{ | ||||
| 		Root:          "root", | ||||
| 		OutputSize:    uint32(size), | ||||
| 		FlagCount:     uint32(128), | ||||
| 		MenuSeparator: menuSeparator, | ||||
| 	} | ||||
| 
 | ||||
| 	if engineDebug { | ||||
| 		cfg.EngineDebug = true | ||||
| 	} | ||||
| 
 | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	err = menuStorageService.EnsureDbDir() | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	userdataStore, err := menuStorageService.GetUserdataDb(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer userdataStore.Close() | ||||
| 
 | ||||
| 	dbResource, ok := rs.(*resource.DbResource) | ||||
| 	if !ok { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	lhs.SetDataStore(&userdataStore) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	accountService := remote.AccountService{} | ||||
| 	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 { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer stateStore.Close() | ||||
| 
 | ||||
| 	rp := &httpserver.DefaultRequestParser{} | ||||
| 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | ||||
| 	sh := httpserver.ToSessionHandler(bsh) | ||||
| 	s := &http.Server{ | ||||
| 		Addr:    fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), | ||||
| 		Handler: sh, | ||||
| 	} | ||||
| 	s.RegisterOnShutdown(sh.Shutdown) | ||||
| 
 | ||||
| 	cint := make(chan os.Signal) | ||||
| 	cterm := make(chan os.Signal) | ||||
| 	signal.Notify(cint, os.Interrupt, syscall.SIGINT) | ||||
| 	signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) | ||||
| 	go func() { | ||||
| 		select { | ||||
| 		case _ = <-cint: | ||||
| 		case _ = <-cterm: | ||||
| 		} | ||||
| 		s.Shutdown(ctx) | ||||
| 	}() | ||||
| 	err = s.ListenAndServe() | ||||
| 	if err != nil { | ||||
| 		logg.Infof("Server closed with error", "err", err) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										120
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								cmd/main.go
									
									
									
									
									
								
							| @ -1,120 +0,0 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 
 | ||||
| 	"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/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg          = logging.NewVanilla() | ||||
| 	scriptDir     = path.Join("services", "registration") | ||||
| 	menuSeparator = ": " | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	initializers.LoadEnvVariables() | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var dbDir string | ||||
| 	var size uint | ||||
| 	var sessionId string | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	logg.Infof("start command", "dbdir", dbDir, "outputsize", size) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 	pfp := path.Join(scriptDir, "pp.csv") | ||||
| 
 | ||||
| 	cfg := engine.Config{ | ||||
| 		Root:          "root", | ||||
| 		SessionId:     sessionId, | ||||
| 		OutputSize:    uint32(size), | ||||
| 		FlagCount:     uint32(128), | ||||
| 		MenuSeparator: menuSeparator, | ||||
| 	} | ||||
| 
 | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 
 | ||||
| 	err := menuStorageService.EnsureDbDir() | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	pe, err := menuStorageService.GetPersister(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	userdatastore, err := menuStorageService.GetUserdataDb(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	dbResource, ok := rs.(*resource.DbResource) | ||||
| 	if !ok { | ||||
| 		fmt.Fprintf(os.Stderr, err.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) | ||||
| 	} | ||||
| 
 | ||||
| 	accountService := remote.AccountService{} | ||||
| 	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) | ||||
| 	if engineDebug { | ||||
| 		en = en.WithDebug(nil) | ||||
| 	} | ||||
| 
 | ||||
| 	err = engine.Loop(ctx, en, os.Stdin, os.Stdout, nil) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										130
									
								
								common/db.go
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								common/db.go
									
									
									
									
									
								
							| @ -1,130 +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 | ||||
| ) | ||||
| 
 | ||||
| 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,33 +0,0 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"regexp" | ||||
| 
 | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
| 
 | ||||
| // Define the regex pattern as a constant
 | ||||
| const ( | ||||
| 	pinPattern = `^\d{4}$` | ||||
| ) | ||||
| 
 | ||||
| // 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,53 +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) | ||||
| 	EnsureDbDir() error | ||||
| } | ||||
| 
 | ||||
| type StorageService struct { | ||||
| 	svc *storage.MenuStorageService | ||||
| } | ||||
| 
 | ||||
| func NewStorageService(dbDir string) *StorageService { | ||||
| 	return &StorageService{ | ||||
| 		svc: storage.NewMenuStorageService(dbDir, ""), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 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") | ||||
| } | ||||
| 
 | ||||
| func(ss *StorageService) EnsureDbDir() error { | ||||
| 	return ss.svc.EnsureDbDir() | ||||
| } | ||||
| @ -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,34 +0,0 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| ) | ||||
| 
 | ||||
| type DataStore interface { | ||||
| 	db.Db | ||||
| 	ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) | ||||
| 	WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error | ||||
| } | ||||
| 
 | ||||
| type UserDataStore struct { | ||||
| 	db.Db | ||||
| } | ||||
| 
 | ||||
| // ReadEntry retrieves an entry to the userdata store.
 | ||||
| func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { | ||||
| 	store.SetPrefix(db.DATATYPE_USERDATA) | ||||
| 	store.SetSession(sessionId) | ||||
| 	k := ToBytes(typ) | ||||
| 	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 { | ||||
| 	store.SetPrefix(db.DATATYPE_USERDATA) | ||||
| 	store.SetSession(sessionId) | ||||
| 	k := ToBytes(typ) | ||||
| 	return store.Put(ctx, k, value) | ||||
| } | ||||
| @ -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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										192
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								config/config.go
									
									
									
									
									
								
							| @ -1,74 +1,164 @@ | ||||
| package config | ||||
| 
 | ||||
| import ( | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	createAccountPath          = "/api/v2/account/create" | ||||
| 	trackStatusPath            = "/api/track" | ||||
| 	balancePathPrefix          = "/api/account" | ||||
| 	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" | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/env" | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/storage" | ||||
| 	slogging "github.com/grassrootseconomics/go-vise/slog" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	custodialURLBase string | ||||
| 	dataURLBase      string | ||||
| 	BearerToken      string | ||||
| 	logg               = slogging.Get().With("component", "visedriver-config") | ||||
| 	defaultLanguage    = "eng" | ||||
| 	languages          []string | ||||
| 	DefaultLanguage    string | ||||
| 	dbConn             string | ||||
| 	dbConnMissing      bool | ||||
| 	dbConnMode         storage.DbMode | ||||
| 	stateDbConn        string | ||||
| 	stateDbConnMode    storage.DbMode | ||||
| 	resourceDbConn     string | ||||
| 	resourceDbConnMode storage.DbMode | ||||
| 	userDbConn         string | ||||
| 	userDbConnMode     storage.DbMode | ||||
| 	Languages          []string | ||||
| 	configManager      *Config | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	CreateAccountURL    string | ||||
| 	TrackStatusURL      string | ||||
| 	BalanceURL          string | ||||
| 	TrackURL            string | ||||
| 	TokenTransferURL    string | ||||
| 	VoucherHoldingsURL  string | ||||
| 	VoucherTransfersURL string | ||||
| 	VoucherDataURL      string | ||||
| 	CheckAliasURL       string | ||||
| ) | ||||
| type Override struct { | ||||
| 	DbConn           string | ||||
| 	DbConnMode       storage.DbMode | ||||
| 	StateConn        string | ||||
| 	StateConnMode    storage.DbMode | ||||
| 	ResourceConn     string | ||||
| 	ResourceConnMode storage.DbMode | ||||
| 	UserConn         string | ||||
| 	UserConnMode     storage.DbMode | ||||
| } | ||||
| 
 | ||||
| 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.JoinPath(custodialURLBase, "/foo") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| func setLanguage() error { | ||||
| 	defaultLanguage = env.GetEnv("DEFAULT_LANGUAGE", defaultLanguage) | ||||
| 	languages = strings.Split(env.GetEnv("LANGUAGES", defaultLanguage), ",") | ||||
| 	haveDefaultLanguage := false | ||||
| 	for i, v := range languages { | ||||
| 		languages[i] = strings.ReplaceAll(v, " ", "") | ||||
| 		if languages[i] == defaultLanguage { | ||||
| 			haveDefaultLanguage = true | ||||
| 		} | ||||
| 	} | ||||
| 	_, err = url.JoinPath(dataURLBase, "/bar") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 
 | ||||
| 	if !haveDefaultLanguage { | ||||
| 		languages = append([]string{defaultLanguage}, languages...) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func setConn() error { | ||||
| 	dbConn = env.GetEnv("DB_CONN", "?") | ||||
| 	stateDbConn = env.GetEnv("DB_CONN_STATE", dbConn) | ||||
| 	resourceDbConn = env.GetEnv("DB_CONN_RESOURCE", dbConn) | ||||
| 	userDbConn = env.GetEnv("DB_CONN_USER", dbConn) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func ApplyConn(override *Override) { | ||||
| 	if override.DbConn != "?" { | ||||
| 		dbConn = override.DbConn | ||||
| 		stateDbConn = override.StateConn | ||||
| 		resourceDbConn = override.ResourceConn | ||||
| 		userDbConn = override.UserConn | ||||
| 	} | ||||
| 	dbConnMode = override.DbConnMode | ||||
| 	if override.StateConn != "?" { | ||||
| 		stateDbConn = override.StateConn | ||||
| 	} | ||||
| 	if override.ResourceConn != "?" { | ||||
| 		resourceDbConn = override.ResourceConn | ||||
| 	} | ||||
| 	if override.UserConn != "?" { | ||||
| 		userDbConn = override.UserConn | ||||
| 	} | ||||
| 
 | ||||
| 	if dbConn == "?" { | ||||
| 		dbConn = "" | ||||
| 	} | ||||
| 
 | ||||
| 	if stateDbConn == "?" { | ||||
| 		stateDbConn = dbConn | ||||
| 		stateDbConnMode = dbConnMode | ||||
| 	} | ||||
| 	if resourceDbConn == "?" { | ||||
| 		resourceDbConn = dbConn | ||||
| 		resourceDbConnMode = dbConnMode | ||||
| 	} | ||||
| 	if userDbConn == "?" { | ||||
| 		userDbConn = dbConn | ||||
| 		userDbConnMode = dbConnMode | ||||
| 	} | ||||
| 
 | ||||
| 	logg.Debugf("conns", "conn", dbConn, "user", userDbConn) | ||||
| 	if override.DbConnMode != storage.DBMODE_ANY { | ||||
| 		dbConnMode = override.DbConnMode | ||||
| 	} | ||||
| 	if override.StateConnMode != storage.DBMODE_ANY { | ||||
| 		stateDbConnMode = override.StateConnMode | ||||
| 	} | ||||
| 	if override.ResourceConnMode != storage.DBMODE_ANY { | ||||
| 		resourceDbConnMode = override.ResourceConnMode | ||||
| 	} | ||||
| 	if override.UserConnMode != storage.DBMODE_ANY { | ||||
| 		userDbConnMode = override.UserConnMode | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetConns() (storage.Conns, error) { | ||||
| 	o := storage.NewConns() | ||||
| 	c, err := storage.ToConnDataMode(stateDbConn, stateDbConnMode) | ||||
| 	if err != nil { | ||||
| 		return o, err | ||||
| 	} | ||||
| 	o.Set(c, storage.STORETYPE_STATE) | ||||
| 	c, err = storage.ToConnDataMode(resourceDbConn, resourceDbConnMode) | ||||
| 	if err != nil { | ||||
| 		return o, err | ||||
| 	} | ||||
| 	o.Set(c, storage.STORETYPE_RESOURCE) | ||||
| 	c, err = storage.ToConnDataMode(userDbConn, userDbConnMode) | ||||
| 	if err != nil { | ||||
| 		return o, err | ||||
| 	} | ||||
| 	o.Set(c, storage.STORETYPE_USER) | ||||
| 	return o, nil | ||||
| } | ||||
| 
 | ||||
| // LoadConfig initializes the configuration values after environment variables are loaded.
 | ||||
| func LoadConfig() error { | ||||
| 	err := setBase() | ||||
| 	configManager = NewConfig() | ||||
| 
 | ||||
| 	// Add configuration keys with validation
 | ||||
| 	configManager.AddKey("HOST", "127.0.0.1", false, nil) | ||||
| 	configManager.AddKey("PORT", "7123", false, func(v string) error { | ||||
| 		_, err := strconv.Atoi(v) | ||||
| 		return err | ||||
| 	}) | ||||
| 	configManager.AddKey("DB_CONN", "", true, nil) | ||||
| 	// ... add other keys ?  or is enough :/ ...
 | ||||
| 
 | ||||
| 	err := setConn() | ||||
| 	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) | ||||
| 	err = setLanguage() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	DefaultLanguage = defaultLanguage | ||||
| 	Languages = languages | ||||
| 
 | ||||
| 	// Report configuration
 | ||||
| 	configManager.Report("INFO") | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										64
									
								
								config/config_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								config/config_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| //go:build configreport
 | ||||
| // +build configreport
 | ||||
| 
 | ||||
| package config | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	slogging "github.com/grassrootseconomics/go-vise/slog" | ||||
| ) | ||||
| 
 | ||||
| // go test -tags configreport ./config/...   ---> run with tag
 | ||||
| func TestConfig(t *testing.T) { | ||||
| 	logger := slogging.Get().With("component", "test") | ||||
| 	cfg := NewConfig(logger) | ||||
| 
 | ||||
| 	t.Run("Default Values", func(t *testing.T) { | ||||
| 		cfg.AddKey("TEST_KEY", "default", false, nil) | ||||
| 		value, err := cfg.GetValue("TEST_KEY") | ||||
| 		t.Logf("Got value: %q, error: %v", value, err) | ||||
| 		if err != nil { | ||||
| 			t.Errorf("unexpected error: %v", err) | ||||
| 		} | ||||
| 		if value != "default" { | ||||
| 			t.Errorf("expected 'default', got '%s'", value) | ||||
| 		} | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Environment Override", func(t *testing.T) { | ||||
| 		os.Setenv("TEST_ENV_KEY", "override") | ||||
| 		defer os.Unsetenv("TEST_ENV_KEY") | ||||
| 
 | ||||
| 		cfg.AddKey("TEST_ENV_KEY", "default", false, nil) | ||||
| 		value, err := cfg.GetValue("TEST_ENV_KEY") | ||||
| 		t.Logf("Got value: %q, error: %v", value, err) | ||||
| 		if err != nil { | ||||
| 			t.Errorf("unexpected error: %v", err) | ||||
| 		} | ||||
| 		if value != "override" { | ||||
| 			t.Errorf("expected 'override', got '%s'", value) | ||||
| 		} | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Validation", func(t *testing.T) { | ||||
| 		validator := func(v string) error { | ||||
| 			if v != "valid" { | ||||
| 				return fmt.Errorf("invalid value") | ||||
| 			} | ||||
| 			return nil | ||||
| 		} | ||||
| 
 | ||||
| 		cfg.AddKey("VALIDATED_KEY", "valid", false, validator) | ||||
| 		os.Setenv("VALIDATED_KEY", "invalid") | ||||
| 		defer os.Unsetenv("VALIDATED_KEY") | ||||
| 
 | ||||
| 		value, err := cfg.GetValue("VALIDATED_KEY") | ||||
| 		t.Logf("Got value: %q, error: %v", value, err) | ||||
| 		if err == nil { | ||||
| 			t.Error("expected validation error, got nil") | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										84
									
								
								config/reporter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								config/reporter.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | ||||
| //go:build configreport
 | ||||
| 
 | ||||
| package config | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/env" | ||||
| 	slogging "github.com/grassrootseconomics/go-vise/slog" | ||||
| ) | ||||
| 
 | ||||
| // ConfigValue represents a configuration key-value pair
 | ||||
| type ConfigValue struct { | ||||
| 	Key       string | ||||
| 	Default   string | ||||
| 	Validator func(string) error | ||||
| 	Sensitive bool | ||||
| } | ||||
| 
 | ||||
| // Config handles configuration management and reporting
 | ||||
| type Config struct { | ||||
| 	values map[string]ConfigValue | ||||
| 	logger *slogging.Slog | ||||
| } | ||||
| 
 | ||||
| func NewConfig(logging interface{}) *Config { | ||||
| 	return &Config{ | ||||
| 		values: make(map[string]ConfigValue), | ||||
| 		logger: slogging.Get().With("component", "visedriver-config-reporter"), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // AddKey registers a new configuration key with optional validation
 | ||||
| func (c *Config) AddKey(key string, defaultValue string, sensitive bool, validator func(string) error) { | ||||
| 	c.values[key] = ConfigValue{ | ||||
| 		Key:       key, | ||||
| 		Default:   defaultValue, | ||||
| 		Validator: validator, | ||||
| 		Sensitive: sensitive, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetValue returns the value for a given key, applying environment override if present
 | ||||
| func (c *Config) GetValue(key string) (string, error) { | ||||
| 	// Find config value by key
 | ||||
| 	var cv ConfigValue | ||||
| 	for _, v := range c.values { | ||||
| 		if v.Key == key { | ||||
| 			cv = v | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if cv.Key == "" { | ||||
| 		return "", fmt.Errorf("configuration key not found: %s", key) | ||||
| 	} | ||||
| 
 | ||||
| 	// Get value from environment or default
 | ||||
| 	value := env.GetEnv(key, cv.Default) | ||||
| 
 | ||||
| 	// Validate if validator exists
 | ||||
| 	if cv.Validator != nil && cv.Validator(value) != nil { | ||||
| 		return "", fmt.Errorf("invalid value for key %s", key) | ||||
| 	} | ||||
| 
 | ||||
| 	return value, nil | ||||
| } | ||||
| 
 | ||||
| // Report outputs all configuration values at the specified log level
 | ||||
| func (c *Config) Report(level string) { | ||||
| 	for _, cv := range c.values { | ||||
| 		value, err := c.GetValue(cv.Key) | ||||
| 		if err != nil { | ||||
| 			c.logger.Errorf("Error getting value for %s: %v", cv.Key, err) | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		if cv.Sensitive { | ||||
| 			value = "****" | ||||
| 		} | ||||
| 
 | ||||
| 		c.logger.Debugf("config set", cv.Key, value) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										18
									
								
								config/reporter_noop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								config/reporter_noop.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| //go:build !configreport
 | ||||
| 
 | ||||
| package config | ||||
| 
 | ||||
| type Config struct{} | ||||
| 
 | ||||
| func NewConfig() *Config { | ||||
| 	return &Config{} | ||||
| } | ||||
| 
 | ||||
| func (c *Config) AddKey(key string, defaultValue string, sensitive bool, validator func(string) error) { | ||||
| } | ||||
| 
 | ||||
| func (c *Config) GetValue(key string) (string, error) { | ||||
| 	return "", nil | ||||
| } | ||||
| 
 | ||||
| func (c *Config) Report(level string) {} | ||||
| @ -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,79 +0,0 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/sha1" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/common" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	initializers.LoadEnvVariables() | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| func main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var dbDir string | ||||
| 	var sessionId string | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 
 | ||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 
 | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, 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) | ||||
| 	} | ||||
| 	 | ||||
| } | ||||
| @ -1,80 +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 main() { | ||||
| 	config.LoadConfig() | ||||
| 
 | ||||
| 	var dbDir string | ||||
| 	var sessionId string | ||||
| 	var database string | ||||
| 	var engineDebug bool | ||||
| 
 | ||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | ||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| 
 | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, 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 | ||||
| 		} | ||||
| 		o, err := debug.FromKey(k) | ||||
| 		if err != nil { | ||||
| 			fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		fmt.Printf("%vValue: %v\n\n", o, string(v)) | ||||
| 	} | ||||
| 
 | ||||
| 	err = store.Close() | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| } | ||||
| @ -10,7 +10,7 @@ This document describes component versions: | ||||
| 
 | ||||
| ## 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. | ||||
| All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `github.com/grassrootseconomics/go-vise/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: | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										14
									
								
								entry/handlers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								entry/handlers.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| package entry | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"github.com/grassrootseconomics/go-vise/persist" | ||||
| 	"github.com/grassrootseconomics/go-vise/resource" | ||||
| ) | ||||
| 
 | ||||
| type EntryHandler interface { | ||||
| 	Init(context.Context, string, []byte) (resource.Result, error) // HandlerFunc
 | ||||
| 	Exit() | ||||
| 	SetPersister(*persist.Persister) | ||||
| } | ||||
							
								
								
									
										18
									
								
								initializers/load.go → env/load.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								initializers/load.go → env/load.go
									
									
									
									
										vendored
									
									
								
							| @ -1,26 +1,32 @@ | ||||
| package initializers | ||||
| package env | ||||
| 
 | ||||
| import ( | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"github.com/joho/godotenv" | ||||
| ) | ||||
| 
 | ||||
| func LoadEnvVariables() { | ||||
| 	err := godotenv.Load() | ||||
| 	LoadEnvVariablesPath(".") | ||||
| } | ||||
| 
 | ||||
| func LoadEnvVariablesPath(dir string) { | ||||
| 	fp := path.Join(dir, ".env") | ||||
| 	err := godotenv.Load(fp) | ||||
| 	if err != nil { | ||||
| 		log.Fatal("Error loading .env file") | ||||
| 		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 | ||||
| 	if value, exists := os.LookupEnv(key); exists { | ||||
| 		return value | ||||
| 	} | ||||
|   return defaultVal | ||||
| 	return defaultVal | ||||
| } | ||||
| 
 | ||||
| // Helper to safely convert environment variables to uint
 | ||||
							
								
								
									
										15
									
								
								errors/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								errors/errors.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| package errors | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrInvalidRequest = errors.New("invalid request for context") | ||||
| 	ErrSessionMissing = errors.New("missing session") | ||||
| 	ErrInvalidInput   = errors.New("invalid input") | ||||
| 	ErrStorage        = errors.New("storage retrieval fail") | ||||
| 	ErrEngineType     = errors.New("incompatible engine") | ||||
| 	ErrEngineInit     = errors.New("engine init fail") | ||||
| 	ErrEngineExec     = errors.New("engine exec fail") | ||||
| ) | ||||
							
								
								
									
										36
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								go.mod
									
									
									
									
									
								
							| @ -1,39 +1,27 @@ | ||||
| module git.grassecon.net/urdt/ussd | ||||
| module git.grassecon.net/grassrootseconomics/visedriver | ||||
| 
 | ||||
| go 1.23.0 | ||||
| go 1.24 | ||||
| 
 | ||||
| toolchain go1.24.6 | ||||
| 
 | ||||
| require ( | ||||
| 	git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d | ||||
| 	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/grassrootseconomics/go-vise v0.5.0 | ||||
| 	github.com/jackc/pgx/v5 v5.7.5 | ||||
| 	github.com/joho/godotenv v1.5.1 | ||||
| 	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 | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/alecthomas/participle/v2 v2.0.0 // indirect | ||||
| 	github.com/alecthomas/repr v0.2.0 // 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/fxamacker/cbor/v2 v2.4.0 // indirect | ||||
| 	github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect | ||||
| 	github.com/hexops/gotextdiff v1.0.3 // indirect | ||||
| 	github.com/fxamacker/cbor/v2 v2.9.0 // 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/leonelquinteros/gotext v1.7.2 // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | ||||
| 	github.com/rogpeppe/go-internal v1.13.1 // indirect | ||||
| 	github.com/stretchr/objx v0.5.2 // indirect | ||||
| 	github.com/stretchr/testify v1.9.0 // 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 | ||||
| 	golang.org/x/crypto v0.40.0 // indirect | ||||
| 	golang.org/x/sync v0.16.0 // indirect | ||||
| 	golang.org/x/text v0.27.0 // indirect | ||||
| ) | ||||
|  | ||||
							
								
								
									
										63
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								go.sum
									
									
									
									
									
								
							| @ -1,74 +1,47 @@ | ||||
| git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw= | ||||
| git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= | ||||
| 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/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= | ||||
| github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y= | ||||
| 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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||||
| github.com/davecgh/go-spew v1.1.0/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/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/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= | ||||
| 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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= | ||||
| github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= | ||||
| github.com/grassrootseconomics/go-vise v0.5.0 h1:FRg2de55Eb5SisrgTBeFWfWX+sXwp5q9r7YWtKWDwsk= | ||||
| github.com/grassrootseconomics/go-vise v0.5.0/go.mod h1:b2/q4jfTu2i1wyUwYUu7FYq4m2f1AZv8MpiDM3ZcoGo= | ||||
| 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/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= | ||||
| github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= | ||||
| 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/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/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc= | ||||
| github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8= | ||||
| github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= | ||||
| github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= | ||||
| github.com/pashagolub/pgxmock/v4 v4.7.0 h1:de2ORuFYyjwOQR7NBm57+321RnZxpYiuUjsmqRiqgh8= | ||||
| github.com/pashagolub/pgxmock/v4 v4.7.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM= | ||||
| 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/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/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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= | ||||
| 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= | ||||
| 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= | ||||
| golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= | ||||
| golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= | ||||
| golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= | ||||
| golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | ||||
| golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= | ||||
| golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= | ||||
| 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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
|  | ||||
| @ -1,108 +0,0 @@ | ||||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"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/ussd" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| ) | ||||
| 
 | ||||
| type BaseSessionHandler struct { | ||||
| 	cfgTemplate engine.Config | ||||
| 	rp RequestParser | ||||
| 	rs resource.Resource | ||||
| 	hn *ussd.Handlers | ||||
| 	provider storage.StorageProvider | ||||
| } | ||||
| 
 | ||||
| func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler { | ||||
| 	return &BaseSessionHandler{ | ||||
| 		cfgTemplate: cfg, | ||||
| 		rs: rs, | ||||
| 		hn: hn, | ||||
| 		rp: rp, | ||||
| 		provider: storage.NewSimpleStorageProvider(stateDb, userdataDb), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func(f* BaseSessionHandler) Shutdown() { | ||||
| 	err := f.provider.Close() | ||||
| 	if err != nil { | ||||
| 		logg.Errorf("handler shutdown error", "err", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine { | ||||
| 	en := engine.NewEngine(cfg, rs) | ||||
| 	en = en.WithPersister(pr) | ||||
| 	return en | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) { | ||||
| 	var r bool | ||||
| 	var err error | ||||
| 	var ok bool | ||||
| 	 | ||||
| 	logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs) | ||||
| 
 | ||||
| 	rqs.Storage, err = f.provider.Get(rqs.Config.SessionId) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err) | ||||
| 		return rqs, ErrStorage | ||||
| 	} | ||||
| 
 | ||||
| 	f.hn = f.hn.WithPersister(rqs.Storage.Persister) | ||||
| 	defer func() { | ||||
| 		f.hn.Exit() | ||||
| 	}() | ||||
| 	eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister) | ||||
| 	en, ok := eni.(*engine.DefaultEngine) | ||||
| 	if !ok { | ||||
| 		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, ErrEngineType | ||||
| 	} | ||||
| 	en = en.WithFirst(f.hn.Init) | ||||
| 	if rqs.Config.EngineDebug { | ||||
| 		en = en.WithDebug(nil) | ||||
| 	} | ||||
| 	rqs.Engine = en | ||||
| 
 | ||||
| 	r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input) | ||||
| 	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 | ||||
| 	} | ||||
| 
 | ||||
| 	rqs.Continue = r  | ||||
| 	return rqs, nil | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession,  error) { | ||||
| 	var err error | ||||
| 	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) | ||||
| 	return rqs, err | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) { | ||||
| 	defer f.provider.Put(rqs.Config.SessionId, rqs.Storage) | ||||
| 	return rqs, rqs.Engine.Finish() | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) GetConfig() engine.Config { | ||||
| 	return f.cfgTemplate | ||||
| } | ||||
| 
 | ||||
| func(f *BaseSessionHandler) GetRequestParser() RequestParser { | ||||
| 	return f.rp | ||||
| } | ||||
| @ -1,140 +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/ussd" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/utils" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| type HandlerService interface { | ||||
| 	GetHandler() (*ussd.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) (*ussd.Handlers, error) { | ||||
| 	replaceSeparatorFunc := func(input string) string { | ||||
| 		return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator) | ||||
| 	} | ||||
| 
 | ||||
| 	ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	ussdHandlers = ussdHandlers.WithPersister(ls.Pe) | ||||
| 	ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) | ||||
| 	ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) | ||||
| 	ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin) | ||||
| 	ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) | ||||
| 	ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) | ||||
| 	ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) | ||||
| 	ls.DbRs.AddLocalFunc("quit", ussdHandlers.Quit) | ||||
| 	ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) | ||||
| 	ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) | ||||
| 	ls.DbRs.AddLocalFunc("invite_valid_recipient", ussdHandlers.InviteValidRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) | ||||
| 	ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) | ||||
| 	ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) | ||||
| 	ls.DbRs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("get_sender", ussdHandlers.GetSender) | ||||
| 	ls.DbRs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) | ||||
| 	ls.DbRs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) | ||||
| 	ls.DbRs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) | ||||
| 	ls.DbRs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) | ||||
| 	ls.DbRs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) | ||||
| 	ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) | ||||
| 	ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) | ||||
| 	ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) | ||||
| 	ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) | ||||
| 	ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) | ||||
| 	ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) | ||||
| 	ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) | ||||
| 	ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) | ||||
| 	ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) | ||||
| 	ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) | ||||
| 	ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) | ||||
| 	ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) | ||||
| 	ls.DbRs.AddLocalFunc("fetch_community_balance", ussdHandlers.FetchCommunityBalance) | ||||
| 	ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers) | ||||
| 	ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) | ||||
| 	ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails) | ||||
| 	ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin) | ||||
| 	ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckBlockedNumPinMisMatch) | ||||
| 	ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber) | ||||
| 	ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber) | ||||
| 	ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber) | ||||
| 	ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin) | ||||
| 	ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo) | ||||
| 	ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions) | ||||
| 	ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList) | ||||
| 	ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) | ||||
| 	ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems) | ||||
| 	ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack) | ||||
| 
 | ||||
| 	return ussdHandlers, 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 | ||||
| } | ||||
| @ -1,54 +0,0 @@ | ||||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"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" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg = logging.NewVanilla().WithDomain("handlers") | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrInvalidRequest = errors.New("invalid request for context") | ||||
| 	ErrSessionMissing = errors.New("missing session") | ||||
| 	ErrInvalidInput = errors.New("invalid input") | ||||
| 	ErrStorage = errors.New("storage retrieval fail") | ||||
| 	ErrEngineType = errors.New("incompatible engine") | ||||
| 	ErrEngineInit = errors.New("engine init fail") | ||||
| 	ErrEngineExec = errors.New("engine exec fail") | ||||
| ) | ||||
| 
 | ||||
| type RequestSession struct { | ||||
| 	Ctx context.Context | ||||
| 	Config engine.Config | ||||
| 	Engine engine.Engine | ||||
| 	Input []byte | ||||
| 	Storage *storage.Storage | ||||
| 	Writer io.Writer | ||||
| 	Continue bool | ||||
| } | ||||
| 
 | ||||
| // TODO: seems like can remove this.
 | ||||
| type RequestParser interface { | ||||
| 	GetSessionId(rq any) (string, error) | ||||
| 	GetInput(rq any) ([]byte, error) | ||||
| } | ||||
| 
 | ||||
| type RequestHandler interface { | ||||
| 	GetConfig() engine.Config | ||||
| 	GetRequestParser() RequestParser | ||||
| 	GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine  | ||||
| 	Process(rs RequestSession) (RequestSession, error) | ||||
| 	Output(rs RequestSession) (RequestSession, error) | ||||
| 	Reset(rs RequestSession) (RequestSession, error) | ||||
| 	Shutdown() | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,121 +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 { | ||||
| 	Context context.Context | ||||
| } | ||||
| 
 | ||||
| func (arp *ATRequestParser) GetSessionId(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 { | ||||
| 			context.WithValue(arp.Context, "at-session-id", sessionId) | ||||
| 		} | ||||
| 		logg.Debugf("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") | ||||
| 	} | ||||
| 
 | ||||
| 	return []byte(parts[len(parts)-1]), 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,98 +0,0 @@ | ||||
| package at | ||||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg = logging.NewVanilla().WithDomain("atserver") | ||||
| ) | ||||
| 
 | ||||
| type ATSessionHandler struct { | ||||
| 	*httpserver.SessionHandler | ||||
| } | ||||
| 
 | ||||
| func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler { | ||||
| 	return &ATSessionHandler{ | ||||
| 		SessionHandler: httpserver.ToSessionHandler(h), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	var code int | ||||
| 	var err error | ||||
| 
 | ||||
| 	rqs := handlers.RequestSession{ | ||||
| 		Ctx:    req.Context(), | ||||
| 		Writer: w, | ||||
| 	} | ||||
| 
 | ||||
| 	rp := ash.GetRequestParser() | ||||
| 	cfg := ash.GetConfig() | ||||
| 	cfg.SessionId, err = rp.GetSessionId(req) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||
| 		ash.WriteError(w, 400, err) | ||||
| 		return | ||||
| 	} | ||||
| 	rqs.Config = cfg | ||||
| 	rqs.Input, err = rp.GetInput(req) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||
| 		ash.WriteError(w, 400, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	rqs, err = ash.Process(rqs)  | ||||
| 	switch err { | ||||
| 	case nil: // set code to 200 if no err
 | ||||
| 		code = 200 | ||||
| 	case handlers.ErrStorage, handlers.ErrEngineInit, handlers.ErrEngineExec, handlers.ErrEngineType: | ||||
| 		code = 500 | ||||
| 	default: | ||||
| 		code = 500 | ||||
| 	} | ||||
| 
 | ||||
| 	if code != 200 { | ||||
| 		ash.WriteError(w, 500, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	w.WriteHeader(200) | ||||
| 	w.Header().Set("Content-Type", "text/plain") | ||||
| 	rqs, err = ash.Output(rqs) | ||||
| 	if err != nil { | ||||
| 		ash.WriteError(w, 500, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	rqs, err = ash.Reset(rqs) | ||||
| 	if err != nil { | ||||
| 		ash.WriteError(w, 500, err) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.RequestSession, error) { | ||||
| 	var err error | ||||
| 	var prefix string | ||||
| 
 | ||||
| 	if rqs.Continue { | ||||
| 		prefix = "CON " | ||||
| 	} else { | ||||
| 		prefix = "END " | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = io.WriteString(rqs.Writer, prefix) | ||||
| 	if err != nil { | ||||
| 		return rqs, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) | ||||
| 	return rqs, err | ||||
| } | ||||
| @ -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,91 +0,0 @@ | ||||
| package http | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg = logging.NewVanilla().WithDomain("httpserver") | ||||
| ) | ||||
| 
 | ||||
| type SessionHandler struct { | ||||
| 	handlers.RequestHandler | ||||
| } | ||||
| 
 | ||||
| func ToSessionHandler(h handlers.RequestHandler) *SessionHandler { | ||||
| 	return &SessionHandler{ | ||||
| 		RequestHandler: h, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (f *SessionHandler) WriteError(w http.ResponseWriter, code int, err error) { | ||||
| 	s := err.Error() | ||||
| 	w.Header().Set("Content-Length", strconv.Itoa(len(s))) | ||||
| 	w.WriteHeader(code) | ||||
| 	_, err = w.Write([]byte(s)) | ||||
| 	if err != nil { | ||||
| 		logg.Errorf("error writing error!!", "err", err, "olderr", s) | ||||
| 		w.WriteHeader(500) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	var code int | ||||
| 	var err error | ||||
| 	var perr error | ||||
| 
 | ||||
| 	rqs := handlers.RequestSession{ | ||||
| 		Ctx:    req.Context(), | ||||
| 		Writer: w, | ||||
| 	} | ||||
| 
 | ||||
| 	rp := f.GetRequestParser() | ||||
| 	cfg := f.GetConfig() | ||||
| 	cfg.SessionId, err = rp.GetSessionId(req) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||
| 		f.WriteError(w, 400, err) | ||||
| 	} | ||||
| 	rqs.Config = cfg | ||||
| 	rqs.Input, err = rp.GetInput(req) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||
| 		f.WriteError(w, 400, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	rqs, err = f.Process(rqs) | ||||
| 	switch err { | ||||
| 	case handlers.ErrStorage: | ||||
| 		code = 500 | ||||
| 	case handlers.ErrEngineInit: | ||||
| 		code = 500 | ||||
| 	case handlers.ErrEngineExec: | ||||
| 		code = 500 | ||||
| 	default: | ||||
| 		code = 200 | ||||
| 	} | ||||
| 
 | ||||
| 	if code != 200 { | ||||
| 		f.WriteError(w, 500, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	w.WriteHeader(200) | ||||
| 	w.Header().Set("Content-Type", "text/plain") | ||||
| 	rqs, err = f.Output(rqs) | ||||
| 	rqs, perr = f.Reset(rqs) | ||||
| 	if err != nil { | ||||
| 		f.WriteError(w, 500, err) | ||||
| 		return | ||||
| 	} | ||||
| 	if perr != nil { | ||||
| 		f.WriteError(w, 500, perr) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
| @ -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,44 +0,0 @@ | ||||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| 	"git.defalsify.org/vise.git/persist" | ||||
| ) | ||||
| 
 | ||||
| type Storage struct { | ||||
| 	Persister *persist.Persister | ||||
| 	UserdataDb db.Db	 | ||||
| } | ||||
| 
 | ||||
| type StorageProvider interface { | ||||
| 	Get(sessionId string) (*Storage, error) | ||||
| 	Put(sessionId string, storage *Storage) error | ||||
| 	Close() error | ||||
| } | ||||
| 
 | ||||
| type SimpleStorageProvider struct { | ||||
| 	*Storage | ||||
| } | ||||
| 
 | ||||
| func NewSimpleStorageProvider(stateStore db.Db, userdataStore db.Db) StorageProvider { | ||||
| 	pe := persist.NewPersister(stateStore) | ||||
| 	pe = pe.WithFlush() | ||||
| 	return &SimpleStorageProvider{ | ||||
| 		Storage: &Storage{ | ||||
| 			Persister: pe, | ||||
| 			UserdataDb: userdataStore, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (p *SimpleStorageProvider) Get(sessionId string) (*Storage, error) { | ||||
| 	return p.Storage, nil | ||||
| } | ||||
| 
 | ||||
| func (p *SimpleStorageProvider) Put(sessionId string, storage *Storage) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (p *SimpleStorageProvider) Close() error { | ||||
| 	return p.Storage.UserdataDb.Close() | ||||
| } | ||||
| @ -1,156 +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/logging" | ||||
| 	"git.defalsify.org/vise.git/persist" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm" | ||||
| ) | ||||
| 
 | ||||
| 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) | ||||
| 	EnsureDbDir() error | ||||
| } | ||||
| 
 | ||||
| type MenuStorageService struct { | ||||
| 	dbDir         string | ||||
| 	resourceDir   string | ||||
| 	resourceStore db.Db | ||||
| 	stateStore    db.Db | ||||
| 	userDataStore db.Db | ||||
| } | ||||
| 
 | ||||
| func buildConnStr() string { | ||||
| 	host := initializers.GetEnv("DB_HOST", "localhost") | ||||
| 	user := initializers.GetEnv("DB_USER", "postgres") | ||||
| 	password := initializers.GetEnv("DB_PASSWORD", "") | ||||
| 	dbName := initializers.GetEnv("DB_NAME", "") | ||||
| 	port := initializers.GetEnv("DB_PORT", "5432") | ||||
| 
 | ||||
| 	connString := fmt.Sprintf( | ||||
| 		"postgres://%s:%s@%s:%s/%s", | ||||
| 		user, password, host, port, dbName, | ||||
| 	) | ||||
| 	logg.Debugf("pg conn string", "conn", connString) | ||||
| 
 | ||||
| 	return connString | ||||
| } | ||||
| 
 | ||||
| func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { | ||||
| 	return &MenuStorageService{ | ||||
| 		dbDir:       dbDir, | ||||
| 		resourceDir: resourceDir, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) { | ||||
| 	database, ok := ctx.Value("Database").(string) | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("failed to select the database") | ||||
| 	} | ||||
| 
 | ||||
| 	if existingDb != nil { | ||||
| 		return existingDb, nil | ||||
| 	} | ||||
| 
 | ||||
| 	var newDb db.Db | ||||
| 	var err error | ||||
| 
 | ||||
| 	if database == "postgres" { | ||||
| 		newDb = postgres.NewPgDb() | ||||
| 		connStr := buildConnStr() | ||||
| 		err = newDb.Connect(ctx, connStr) | ||||
| 	} else { | ||||
| 		newDb = gdbmstorage.NewThreadGdbmDb() | ||||
| 		storeFile := path.Join(ms.dbDir, fileName) | ||||
| 		err = newDb.Connect(ctx, storeFile) | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return newDb, 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) | ||||
| 	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.dbDir, 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,124 +0,0 @@ | ||||
| package testutil | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 	"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" | ||||
| 	testdataloader "github.com/peteole/testdata-loader" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	baseDir   = testdataloader.GetBasePath() | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join(baseDir, "services", "registration") | ||||
| ) | ||||
| 
 | ||||
| func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 	ctx = context.WithValue(ctx, "Database", "gdbm") | ||||
| 	pfp := path.Join(scriptDir, "pp.csv") | ||||
| 
 | ||||
| 	var eventChannel = make(chan bool) | ||||
| 
 | ||||
| 	cfg := engine.Config{ | ||||
| 		Root:       "root", | ||||
| 		SessionId:  sessionId, | ||||
| 		OutputSize: uint32(160), | ||||
| 		FlagCount:  uint32(128), | ||||
| 	} | ||||
| 
 | ||||
| 	dbDir := ".test_state" | ||||
| 	resourceDir := scriptDir | ||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | ||||
| 
 | ||||
| 	err := menuStorageService.EnsureDbDir() | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	rs, err := menuStorageService.GetResource(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	pe, err := menuStorageService.GetPersister(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	userDataStore, err := menuStorageService.GetUserdataDb(ctx) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	dbResource, ok := rs.(*resource.DbResource) | ||||
| 	if !ok { | ||||
| 		fmt.Fprintf(os.Stderr, err.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,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,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,56 +0,0 @@ | ||||
| package utils | ||||
| 
 | ||||
| import ( | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // 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.
 | ||||
| func CalculateAge(birthdate, today time.Time) int { | ||||
| 	today = today.In(birthdate.Location()) | ||||
| 	ty, tm, td := today.Date() | ||||
| 	today = time.Date(ty, tm, td, 0, 0, 0, 0, time.UTC) | ||||
| 	by, bm, bd := birthdate.Date() | ||||
| 	birthdate = time.Date(by, bm, bd, 0, 0, 0, 0, time.UTC) | ||||
| 	if today.Before(birthdate) { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	age := ty - by | ||||
| 	anniversary := birthdate.AddDate(age, 0, 0) | ||||
| 	if anniversary.After(today) { | ||||
| 		age-- | ||||
| 	} | ||||
| 	return age | ||||
| } | ||||
| 
 | ||||
| // CalculateAgeWithYOB calculates the age based on the given year of birth (YOB).
 | ||||
| // It subtracts the YOB from the current year to determine the age.
 | ||||
| //
 | ||||
| // Parameters:
 | ||||
| //
 | ||||
| //	yob: The year of birth as an integer.
 | ||||
| //
 | ||||
| // Returns:
 | ||||
| //
 | ||||
| //	The calculated age as an integer.
 | ||||
| func CalculateAgeWithYOB(yob int) int { | ||||
| 	currentYear := time.Now().Year() | ||||
| 	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 | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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,452 +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\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\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\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,383 +0,0 @@ | ||||
| package menutraversaltest | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"flag" | ||||
| 	"log" | ||||
| 	"math/rand" | ||||
| 	"os" | ||||
| 	"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() | ||||
| 	testStore = ".test_state" | ||||
| 	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") | ||||
| 
 | ||||
| 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) { | ||||
| 	sessionID = GenerateSessionId() | ||||
| 	defer func() { | ||||
| 		if err := os.RemoveAll(testStore); err != nil { | ||||
| 			log.Fatalf("Failed to delete state store %s: %v", testStore, err) | ||||
| 		} | ||||
| 	}() | ||||
| 	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"` | ||||
| } | ||||
| @ -1,8 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| import "encoding/json" | ||||
| 
 | ||||
| type BalanceResult struct { | ||||
| 	Balance string      `json:"balance"` | ||||
| 	Nonce   json.Number `json:"nonce"` | ||||
| } | ||||
| @ -1,18 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| type Profile struct { | ||||
| 	ProfileItems []string | ||||
| 	Max          int | ||||
| } | ||||
| 
 | ||||
| func (p *Profile) InsertOrShift(index int, value string) { | ||||
| 	if index < len(p.ProfileItems) { | ||||
| 		p.ProfileItems = append(p.ProfileItems[:index], value) | ||||
| 	} else { | ||||
| 		for len(p.ProfileItems) < index { | ||||
| 			p.ProfileItems = append(p.ProfileItems, "0") | ||||
| 		} | ||||
| 		p.ProfileItems = append(p.ProfileItems, "0")  | ||||
| 		p.ProfileItems[index] = value | ||||
| 	} | ||||
| } | ||||
| @ -1,5 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| type TokenTransferResponse struct { | ||||
| 	TrackingId string `json:"trackingId"` | ||||
| } | ||||
| @ -1,18 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Transaction struct { | ||||
| 	CreatedAt     time.Time   `json:"createdAt"` | ||||
| 	Status        string      `json:"status"` | ||||
| 	TransferValue json.Number `json:"transferValue"` | ||||
| 	TxHash        string      `json:"txHash"` | ||||
| 	TxType        string      `json:"txType"` | ||||
| } | ||||
| 
 | ||||
| type TrackStatusResult struct { | ||||
| 	Active bool `json:"active"` | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| type VoucherDataResult struct { | ||||
| 	TokenName      string `json:"tokenName"` | ||||
| 	TokenSymbol    string `json:"tokenSymbol"` | ||||
| 	TokenDecimals  int    `json:"tokenDecimals"` | ||||
| 	SinkAddress    string `json:"sinkAddress"` | ||||
| 	TokenCommodity string `json:"tokenCommodity"` | ||||
| 	TokenLocation  string `json:"tokenLocation"` | ||||
| } | ||||
| @ -1,294 +0,0 @@ | ||||
| package remote | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/models" | ||||
| 	"github.com/grassrootseconomics/eth-custodial/pkg/api" | ||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||
| ) | ||||
| 
 | ||||
| type AccountServiceInterface interface { | ||||
| 	CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) | ||||
| 	CreateAccount(ctx context.Context) (*models.AccountResult, error) | ||||
| 	TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) | ||||
| 	FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) | ||||
| 	FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) | ||||
| 	VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) | ||||
| 	TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) | ||||
| 	CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) | ||||
| } | ||||
| 
 | ||||
| type AccountService struct { | ||||
| } | ||||
| 
 | ||||
| // 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) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { | ||||
| 	var r models.TrackStatusResult | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.TrackURL, publicKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &r, 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(ctx context.Context, publicKey string) (*models.BalanceResult, error) { | ||||
| 	var balanceResult models.BalanceResult | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.BalanceURL, publicKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &balanceResult) | ||||
| 	return &balanceResult, err | ||||
| } | ||||
| 
 | ||||
| // 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(ctx context.Context) (*models.AccountResult, error) { | ||||
| 	var r models.AccountResult | ||||
| 	// Create a new request
 | ||||
| 	req, err := http.NewRequest("POST", config.CreateAccountURL, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &r, nil | ||||
| } | ||||
| 
 | ||||
| // FetchVouchers retrieves the token holdings for a given public key from the data indexer API endpoint
 | ||||
| // Parameters:
 | ||||
| //   - publicKey: The public key associated with the account.
 | ||||
| func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { | ||||
| 	var r struct { | ||||
| 		Holdings []dataserviceapi.TokenHoldings `json:"holdings"` | ||||
| 	} | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return r.Holdings, nil | ||||
| } | ||||
| 
 | ||||
| // FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint
 | ||||
| // Parameters:
 | ||||
| //   - publicKey: The public key associated with the account.
 | ||||
| func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { | ||||
| 	var r struct { | ||||
| 		Transfers []dataserviceapi.Last10TxResponse `json:"transfers"` | ||||
| 	} | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return r.Transfers, nil | ||||
| } | ||||
| 
 | ||||
| // VoucherData retrieves voucher metadata from the data indexer API endpoint.
 | ||||
| // Parameters:
 | ||||
| //   - address: The voucher address.
 | ||||
| func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { | ||||
| 	var r struct { | ||||
| 		TokenDetails models.VoucherDataResult `json:"tokenDetails"` | ||||
| 	} | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.VoucherDataURL, address) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	return &r.TokenDetails, err | ||||
| } | ||||
| 
 | ||||
| // TokenTransfer creates a new token transfer in the custodial system.
 | ||||
| // Returns:
 | ||||
| //   - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId.
 | ||||
| //     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) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { | ||||
| 	var r models.TokenTransferResponse | ||||
| 
 | ||||
| 	// Create request payload
 | ||||
| 	payload := map[string]string{ | ||||
| 		"amount":       amount, | ||||
| 		"from":         from, | ||||
| 		"to":           to, | ||||
| 		"tokenAddress": tokenAddress, | ||||
| 	} | ||||
| 
 | ||||
| 	payloadBytes, err := json.Marshal(payload) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Create a new request
 | ||||
| 	req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &r, nil | ||||
| } | ||||
| 
 | ||||
| // CheckAliasAddress retrieves the address of an alias from the API endpoint.
 | ||||
| // Parameters:
 | ||||
| //   - alias: The alias of the user.
 | ||||
| func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { | ||||
| 	var r dataserviceapi.AliasAddress | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.CheckAliasURL, alias) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	return &r, err | ||||
| } | ||||
| 
 | ||||
| func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { | ||||
| 	var okResponse api.OKResponse | ||||
| 	var errResponse api.ErrResponse | ||||
| 
 | ||||
| 	req.Header.Set("Authorization", "Bearer "+config.BearerToken) | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
| 
 | ||||
| 	logRequestDetails(req) | ||||
| 
 | ||||
| 	resp, err := http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error()) | ||||
| 		errResponse.Description = err.Error() | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type")) | ||||
| 	body, err := io.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if resp.StatusCode >= http.StatusBadRequest { | ||||
| 		err := json.Unmarshal([]byte(body), &errResponse) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return nil, errors.New(errResponse.Description) | ||||
| 	} | ||||
| 	err = json.Unmarshal([]byte(body), &okResponse) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(okResponse.Result) == 0 { | ||||
| 		return nil, errors.New("Empty api result") | ||||
| 	} | ||||
| 
 | ||||
| 	v, err := json.Marshal(okResponse.Result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	err = json.Unmarshal(v, &rcpt) | ||||
| 	return &okResponse, err | ||||
| } | ||||
| 
 | ||||
| func logRequestDetails(req *http.Request) { | ||||
| 	var bodyBytes []byte | ||||
| 	contentType := req.Header.Get("Content-Type") | ||||
| 	if req.Body != nil { | ||||
| 		bodyBytes, err := io.ReadAll(req.Body) | ||||
| 		if err != nil { | ||||
| 			log.Printf("Error reading request body: %s", err) | ||||
| 			return | ||||
| 		} | ||||
| 		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) | ||||
| 	} else { | ||||
| 		bodyBytes = []byte("-") | ||||
| 	} | ||||
| 
 | ||||
| 	log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes)) | ||||
| } | ||||
							
								
								
									
										113
									
								
								request/base.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								request/base.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,113 @@ | ||||
| package request | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"github.com/grassrootseconomics/go-vise/db" | ||||
| 	"github.com/grassrootseconomics/go-vise/engine" | ||||
| 	"github.com/grassrootseconomics/go-vise/persist" | ||||
| 	"github.com/grassrootseconomics/go-vise/resource" | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/entry" | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/errors" | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/storage" | ||||
| ) | ||||
| 
 | ||||
| type EngineFunc func(engine.Config, resource.Resource, *persist.Persister) engine.Engine | ||||
| 
 | ||||
| type BaseRequestHandler struct { | ||||
| 	cfgTemplate engine.Config | ||||
| 	rp          RequestParser | ||||
| 	rs          resource.Resource | ||||
| 	hn          entry.EntryHandler | ||||
| 	provider    storage.StorageProvider | ||||
| 	engineFunc	EngineFunc | ||||
| } | ||||
| 
 | ||||
| func NewBaseRequestHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn entry.EntryHandler) *BaseRequestHandler { | ||||
| 	h := &BaseRequestHandler{ | ||||
| 		cfgTemplate: cfg, | ||||
| 		rs:          rs, | ||||
| 		hn:          hn, | ||||
| 		rp:          rp, | ||||
| 		provider:    storage.NewSimpleStorageProvider(stateDb, userdataDb), | ||||
| 	} | ||||
| 	h.engineFunc = h.getDefaultEngine | ||||
| 	return h | ||||
| } | ||||
| 
 | ||||
| func (f *BaseRequestHandler) WithEngineFunc(fn EngineFunc) *BaseRequestHandler { | ||||
| 	f.engineFunc = fn | ||||
| 	return f | ||||
| } | ||||
| 
 | ||||
| func (f *BaseRequestHandler) Shutdown(ctx context.Context) { | ||||
| 	err := f.provider.Close(ctx) | ||||
| 	if err != nil { | ||||
| 		logg.Errorf("handler shutdown error", "err", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (f *BaseRequestHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine { | ||||
| 	return f.engineFunc(cfg, rs, pr) | ||||
| } | ||||
| 
 | ||||
| func (f *BaseRequestHandler) getDefaultEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine { | ||||
| 	en := engine.NewEngine(cfg, rs) | ||||
| 	en = en.WithPersister(pr) | ||||
| 	en = en.WithFirst(f.hn.Init) | ||||
| 	if f.cfgTemplate.EngineDebug { | ||||
| 		en = en.WithDebug(nil) | ||||
| 	} | ||||
| 	return en | ||||
| } | ||||
| 
 | ||||
| func (f *BaseRequestHandler) Process(rqs RequestSession) (RequestSession, error) { | ||||
| 	var r bool | ||||
| 	var err error | ||||
| 
 | ||||
| 	logg.InfoCtxf(rqs.Ctx, "new request", "data", rqs) | ||||
| 
 | ||||
| 	rqs.Storage, err = f.provider.Get(rqs.Ctx, rqs.Config.SessionId) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "storage get error", err) | ||||
| 		return rqs, errors.ErrStorage | ||||
| 	} | ||||
| 
 | ||||
| 	f.hn.SetPersister(rqs.Storage.Persister) | ||||
| 	defer func() { | ||||
| 		f.hn.Exit() | ||||
| 	}() | ||||
| 
 | ||||
| 	rqs.Engine = f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister) | ||||
| 	r, err = rqs.Engine.Exec(rqs.Ctx, rqs.Input) | ||||
| 	if err != nil { | ||||
| 		perr := f.provider.Put(rqs.Ctx, rqs.Config.SessionId, rqs.Storage) | ||||
| 		rqs.Storage = nil | ||||
| 		if perr != nil { | ||||
| 			logg.ErrorCtxf(rqs.Ctx, "", "storage put error", perr) | ||||
| 		} | ||||
| 		return rqs, err | ||||
| 	} | ||||
| 
 | ||||
| 	rqs.Continue = r | ||||
| 	return rqs, nil | ||||
| } | ||||
| 
 | ||||
| func (f *BaseRequestHandler) Output(rqs RequestSession) (RequestSession, error) { | ||||
| 	var err error | ||||
| 	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) | ||||
| 	return rqs, err | ||||
| } | ||||
| 
 | ||||
| func (f *BaseRequestHandler) Reset(ctx context.Context, rqs RequestSession) (RequestSession, error) { | ||||
| 	defer f.provider.Put(ctx, rqs.Config.SessionId, rqs.Storage) | ||||
| 	return rqs, rqs.Engine.Finish(ctx) | ||||
| } | ||||
| 
 | ||||
| func (f *BaseRequestHandler) GetConfig() engine.Config { | ||||
| 	return f.cfgTemplate | ||||
| } | ||||
| 
 | ||||
| func (f *BaseRequestHandler) GetRequestParser() RequestParser { | ||||
| 	return f.rp | ||||
| } | ||||
| @ -1,23 +1,24 @@ | ||||
| package http | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/errors" | ||||
| ) | ||||
| 
 | ||||
| type DefaultRequestParser struct { | ||||
| } | ||||
| 
 | ||||
| func (rp *DefaultRequestParser) GetSessionId(rq any) (string, error) { | ||||
| func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) { | ||||
| 	rqv, ok := rq.(*http.Request) | ||||
| 	if !ok { | ||||
| 		return "", handlers.ErrInvalidRequest | ||||
| 		return "", errors.ErrInvalidRequest | ||||
| 	} | ||||
| 	v := rqv.Header.Get("X-Vise-Session") | ||||
| 	if v == "" { | ||||
| 		return "", handlers.ErrSessionMissing | ||||
| 		return "", errors.ErrSessionMissing | ||||
| 	} | ||||
| 	return v, nil | ||||
| } | ||||
| @ -25,7 +26,7 @@ func (rp *DefaultRequestParser) GetSessionId(rq any) (string, error) { | ||||
| func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) { | ||||
| 	rqv, ok := rq.(*http.Request) | ||||
| 	if !ok { | ||||
| 		return nil, handlers.ErrInvalidRequest | ||||
| 		return nil, errors.ErrInvalidRequest | ||||
| 	} | ||||
| 	defer rqv.Body.Close() | ||||
| 	v, err := ioutil.ReadAll(rqv.Body) | ||||
| @ -34,5 +35,3 @@ func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) { | ||||
| 	} | ||||
| 	return v, nil | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										92
									
								
								request/http/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								request/http/server.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | ||||
| package http | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/errors" | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/request" | ||||
| 	slogging "github.com/grassrootseconomics/go-vise/slog" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg = slogging.Get().With("component", "visedriver.http.session") | ||||
| ) | ||||
| 
 | ||||
| // HTTPRequestHandler implements the session handler for HTTP
 | ||||
| type HTTPRequestHandler struct { | ||||
| 	request.RequestHandler | ||||
| } | ||||
| 
 | ||||
| func (f *HTTPRequestHandler) WriteError(w http.ResponseWriter, code int, err error) { | ||||
| 	s := err.Error() | ||||
| 	w.Header().Set("Content-Length", strconv.Itoa(len(s))) | ||||
| 	w.WriteHeader(code) | ||||
| 	_, err = w.Write([]byte(s)) | ||||
| 	if err != nil { | ||||
| 		logg.Errorf("error writing error!!", "err", err, "olderr", s) | ||||
| 		w.WriteHeader(500) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func NewHTTPRequestHandler(h request.RequestHandler) *HTTPRequestHandler { | ||||
| 	return &HTTPRequestHandler{ | ||||
| 		RequestHandler: h, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (hh *HTTPRequestHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	var code int | ||||
| 	var err error | ||||
| 	var perr error | ||||
| 
 | ||||
| 	rqs := request.RequestSession{ | ||||
| 		Ctx:    req.Context(), | ||||
| 		Writer: w, | ||||
| 	} | ||||
| 
 | ||||
| 	rp := hh.GetRequestParser() | ||||
| 	cfg := hh.GetConfig() | ||||
| 	cfg.SessionId, err = rp.GetSessionId(req.Context(), req) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||
| 		hh.WriteError(w, 400, err) | ||||
| 	} | ||||
| 	rqs.Config = cfg | ||||
| 	rqs.Input, err = rp.GetInput(req) | ||||
| 	if err != nil { | ||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||
| 		hh.WriteError(w, 400, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	rqs, err = hh.Process(rqs) | ||||
| 	switch err { | ||||
| 	case errors.ErrStorage: | ||||
| 		code = 500 | ||||
| 	case errors.ErrEngineInit: | ||||
| 		code = 500 | ||||
| 	case errors.ErrEngineExec: | ||||
| 		code = 500 | ||||
| 	default: | ||||
| 		code = 200 | ||||
| 	} | ||||
| 
 | ||||
| 	if code != 200 { | ||||
| 		hh.WriteError(w, 500, err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	w.WriteHeader(200) | ||||
| 	w.Header().Set("Content-Type", "text/plain") | ||||
| 	rqs, err = hh.Output(rqs) | ||||
| 	rqs, perr = hh.Reset(rqs.Ctx, rqs) | ||||
| 	if err != nil { | ||||
| 		hh.WriteError(w, 500, err) | ||||
| 		return | ||||
| 	} | ||||
| 	if perr != nil { | ||||
| 		hh.WriteError(w, 500, perr) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
| @ -2,14 +2,16 @@ 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" | ||||
| 	"github.com/grassrootseconomics/go-vise/engine" | ||||
| 	viseerrors "git.grassecon.net/grassrootseconomics/visedriver/errors" | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/request" | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/testutil/mocks/httpmocks" | ||||
| ) | ||||
| 
 | ||||
| // invalidRequestType is a custom type to test invalid request scenarios
 | ||||
| @ -22,7 +24,7 @@ func (e *errorReader) Read(p []byte) (n int, err error) { | ||||
| 	return 0, errors.New("read error") | ||||
| } | ||||
| 
 | ||||
| func TestSessionHandler_ServeHTTP(t *testing.T) { | ||||
| func TestRequestHandler_ServeHTTP(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		sessionID      string | ||||
| @ -42,14 +44,14 @@ func TestSessionHandler_ServeHTTP(t *testing.T) { | ||||
| 		{ | ||||
| 			name:           "Missing Session ID", | ||||
| 			sessionID:      "", | ||||
| 			parserErr:      handlers.ErrSessionMissing, | ||||
| 			parserErr:      viseerrors.ErrSessionMissing, | ||||
| 			expectedStatus: http.StatusBadRequest, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Process Error", | ||||
| 			sessionID:      "123", | ||||
| 			input:          []byte("test input"), | ||||
| 			processErr:     handlers.ErrStorage, | ||||
| 			processErr:     viseerrors.ErrStorage, | ||||
| 			expectedStatus: http.StatusInternalServerError, | ||||
| 		}, | ||||
| 		{ | ||||
| @ -80,16 +82,16 @@ func TestSessionHandler_ServeHTTP(t *testing.T) { | ||||
| 			} | ||||
| 
 | ||||
| 			mockRequestHandler := &httpmocks.MockRequestHandler{ | ||||
| 				ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { | ||||
| 				ProcessFunc: func(rs request.RequestSession) (request.RequestSession, error) { | ||||
| 					return rs, tt.processErr | ||||
| 				}, | ||||
| 				OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { | ||||
| 				OutputFunc: func(rs request.RequestSession) (request.RequestSession, error) { | ||||
| 					return rs, tt.outputErr | ||||
| 				}, | ||||
| 				ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { | ||||
| 				ResetFunc: func(ctx context.Context, rs request.RequestSession) (request.RequestSession, error) { | ||||
| 					return rs, tt.resetErr | ||||
| 				}, | ||||
| 				GetRequestParserFunc: func() handlers.RequestParser { | ||||
| 				GetRequestParserFunc: func() request.RequestParser { | ||||
| 					return mockRequestParser | ||||
| 				}, | ||||
| 				GetConfigFunc: func() engine.Config { | ||||
| @ -97,7 +99,9 @@ func TestSessionHandler_ServeHTTP(t *testing.T) { | ||||
| 				}, | ||||
| 			} | ||||
| 
 | ||||
| 			sessionHandler := ToSessionHandler(mockRequestHandler) | ||||
| 			sessionHandler := &HTTPRequestHandler{ | ||||
| 				RequestHandler: mockRequestHandler, | ||||
| 			} | ||||
| 
 | ||||
| 			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input)) | ||||
| 			req.Header.Set("X-Vise-Session", tt.sessionID) | ||||
| @ -114,8 +118,8 @@ func TestSessionHandler_ServeHTTP(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestSessionHandler_WriteError(t *testing.T) { | ||||
| 	handler := &SessionHandler{} | ||||
| func TestRequestHandler_WriteError(t *testing.T) { | ||||
| 	handler := &HTTPRequestHandler{} | ||||
| 	mockWriter := &httpmocks.MockWriter{} | ||||
| 	err := errors.New("test error") | ||||
| 
 | ||||
| @ -147,13 +151,13 @@ func TestDefaultRequestParser_GetSessionId(t *testing.T) { | ||||
| 			name:          "Missing Session ID", | ||||
| 			request:       httptest.NewRequest(http.MethodPost, "/", nil), | ||||
| 			expectedID:    "", | ||||
| 			expectedError: handlers.ErrSessionMissing, | ||||
| 			expectedError: viseerrors.ErrSessionMissing, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "Invalid Request Type", | ||||
| 			request:       invalidRequestType{}, | ||||
| 			expectedID:    "", | ||||
| 			expectedError: handlers.ErrInvalidRequest, | ||||
| 			expectedError: viseerrors.ErrInvalidRequest, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| @ -161,7 +165,7 @@ func TestDefaultRequestParser_GetSessionId(t *testing.T) { | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			id, err := parser.GetSessionId(tt.request) | ||||
| 			id, err := parser.GetSessionId(context.Background(), tt.request) | ||||
| 
 | ||||
| 			if id != tt.expectedID { | ||||
| 				t.Errorf("Expected session ID %s, got %s", tt.expectedID, id) | ||||
| @ -199,7 +203,7 @@ func TestDefaultRequestParser_GetInput(t *testing.T) { | ||||
| 			name:          "Invalid Request Type", | ||||
| 			request:       invalidRequestType{}, | ||||
| 			expectedInput: nil, | ||||
| 			expectedError: handlers.ErrInvalidRequest, | ||||
| 			expectedError: viseerrors.ErrInvalidRequest, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Read Error", | ||||
							
								
								
									
										42
									
								
								request/request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								request/request.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| package request | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 
 | ||||
| 	"git.grassecon.net/grassrootseconomics/visedriver/storage" | ||||
| 	"github.com/grassrootseconomics/go-vise/engine" | ||||
| 	"github.com/grassrootseconomics/go-vise/persist" | ||||
| 	"github.com/grassrootseconomics/go-vise/resource" | ||||
| 	slogging "github.com/grassrootseconomics/go-vise/slog" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg = slogging.Get().With("component", "visedriver.request") | ||||
| ) | ||||
| 
 | ||||
| type RequestSession struct { | ||||
| 	Ctx      context.Context | ||||
| 	Config   engine.Config | ||||
| 	Engine   engine.Engine | ||||
| 	Input    []byte | ||||
| 	Storage  *storage.Storage | ||||
| 	Writer   io.Writer | ||||
| 	Continue bool | ||||
| } | ||||
| 
 | ||||
| // TODO: seems like can remove this.
 | ||||
| type RequestParser interface { | ||||
| 	GetSessionId(context.Context, any) (string, error) | ||||
| 	GetInput(any) ([]byte, error) | ||||
| } | ||||
| 
 | ||||
| type RequestHandler interface { | ||||
| 	GetConfig() engine.Config | ||||
| 	GetRequestParser() RequestParser | ||||
| 	GetEngine(engine.Config, resource.Resource, *persist.Persister) engine.Engine | ||||
| 	Process(RequestSession) (RequestSession, error) | ||||
| 	Output(RequestSession) (RequestSession, error) | ||||
| 	Reset(context.Context, RequestSession) (RequestSession, error) | ||||
| 	Shutdown(ctx context.Context) | ||||
| } | ||||
| @ -1,18 +0,0 @@ | ||||
| # Variables to match files in the current directory
 | ||||
| INPUTS = $(wildcard ./*.vis) | ||||
| TXTS = $(wildcard ./*.txt.orig) | ||||
| VISE_PATH := ../../go-vise | ||||
| 
 | ||||
| # Rule to build .bin files from .vis files
 | ||||
| %.vis: | ||||
| 	go run  $(VISE_PATH)/dev/asm/main.go -f pp.csv $(basename $@).vis > $(basename $@).bin | ||||
| 	@echo "Built $(basename $@).bin from $(basename $@).vis" | ||||
| 
 | ||||
| # Rule to copy .orig files to .txt
 | ||||
| %.txt.orig: | ||||
| 	cp -v $(basename $@).orig $(basename $@) | ||||
| 	@echo "Copied $(basename $@).orig to $(basename $@)" | ||||
| 
 | ||||
| # 'all' target depends on all .vis and .txt.orig files
 | ||||
| all: $(INPUTS) $(TXTS) | ||||
| 	@echo "Running all: $(INPUTS) $(TXTS)" | ||||
| @ -1 +0,0 @@ | ||||
| Something went wrong.Please try again | ||||
| @ -1 +0,0 @@ | ||||
| HALT | ||||
| @ -1 +0,0 @@ | ||||
| Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye. | ||||
| @ -1 +0,0 @@ | ||||
| Your account is being created... | ||||
| @ -1,4 +0,0 @@ | ||||
| RELOAD verify_create_pin | ||||
| CATCH create_pin_mismatch flag_pin_mismatch 1 | ||||
| LOAD quit 0 | ||||
| HALT | ||||
| @ -1 +0,0 @@ | ||||
| Your account creation request failed. Please try again later. | ||||
| @ -1,3 +0,0 @@ | ||||
| MOUT quit 9 | ||||
| HALT | ||||
| INCMP quit 9 | ||||
| @ -1 +0,0 @@ | ||||
| Ombi lako la kusajiliwa haliwezi kukamilishwa. Tafadhali jaribu tena baadaye. | ||||
| @ -1 +0,0 @@ | ||||
| Akaunti yako inatengenezwa... | ||||
| @ -1 +0,0 @@ | ||||
| My Account | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user