forked from urdt/ussd
		
	Compare commits
	
		
			340 Commits
		
	
	
		
			sohail/doc
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b11f11b5fa | |||
| 3d35a5de78 | |||
| a19ace85f8 | |||
| 8f5ed0cd4f | |||
| c29abfe21e | |||
| 9a6d8e5158 | |||
| db431c750e | |||
| 2b9c6d641e | |||
| 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 | |||
|  | 599815c343 | ||
|  | 462c0d7677 | ||
| 80b96e9bf6 | |||
| b5561decd1 | |||
|  | 02823fd64e | ||
|  | cd575c2edb | ||
| f3d4f35718 | |||
| 52787bdb4d | |||
| 824d39908b | |||
|  | 52fd1eced2 | ||
| a312ea5b84 | |||
|  | 5c7a535288 | ||
| 4836162f40 | |||
|  | cc2f7b41df | ||
|  | d39740a09a | ||
|  | 2024cc96e2 | ||
|  | d2d878d5d7 | ||
| c995143543 | |||
| 44570e20ef | |||
| 362eb209ef | |||
| c69d3896f1 | |||
| 974af6b2a7 | |||
|  | bb4037e73f | ||
|  | 51b6fc0dde | ||
|  | cc9760125a | ||
|  | 3a9f3fa373 | ||
|  | 89c21847b9 | ||
|  | 450dfa02cc | ||
|  | f61e65f4fe | ||
|  | a4d6cef9c0 | ||
|  | 2992f7ae8e | ||
|  | dc61d05584 | ||
|  | 83857026d3 | ||
|  | 349051b5ef | ||
| 47b5ff0435 | |||
|  | e92e498726 | ||
|  | 25867cf05e | ||
|  | c3cbe1cd92 | ||
|  | 418080d093 | ||
|  | 2e30739ec9 | ||
| d5a2680500 | |||
|  | dc1674ec55 | ||
|  | d950b10b50 | ||
|  | bcb3ab905e | ||
|  | 3ed9caf16d | ||
|  | 86464c31d2 | ||
| 5ee10d8e14 | |||
| 62f3681b9e | |||
| 3ce1435591 | |||
| f65c458daa | |||
|  | 67007fcd48 | ||
|  | f1b258fa6d | ||
|  | daec816a3e | ||
|  | ac0c43cb43 | ||
|  | 9013cc3618 | ||
|  | 056d056613 | ||
|  | e581ec4771 | ||
|  | e16b7445e8 | ||
|  | 1b12f0ba5f | ||
| d2fce05461 | |||
| 68ac237449 | |||
| 162e6c1934 | |||
| 8bd025f2b2 | |||
| 9d6e25e184 | |||
| c26f5683f6 | |||
| 91dc9ce82f | |||
| 0fe48a30fa | |||
|  | c1e0617bb3 | ||
|  | 6723884103 | ||
|  | b888af446d | ||
|  | 43b2c3b78d | ||
|  | d67853f6d9 | ||
| 58edfa01a2 | |||
| 3830c12a57 | |||
| f1fd690a7b | |||
|  | 06230dc557 | ||
| 491b7424a9 | |||
| 29ce4b83bd | |||
| ca8df5989a | |||
| 82b4365d16 | |||
| 98db85511b | |||
| 99a4d3ff42 | |||
| d95c7abea4 | |||
| fd1ac85a1b | |||
| c899c098f6 | |||
| 5ca6a74274 | |||
| 48d63fb43f | |||
|  | 6ee2c88fe2 | ||
| e666c58644 | |||
| e980586910 | |||
| ffd5be1f1f | |||
| ed1aeecf7d | |||
| 3b69f3d38d | |||
|  | cd58f5ae33 | ||
| 7a535f796a | |||
| 7c4c73125e | |||
|  | c7dbe1d88f | ||
| 4ea52bf3fb | |||
| be2ea3a2f0 | |||
| 8217ea8fdc | |||
| 3c73fc7188 | |||
| 1311a0cab9 | |||
|  | 3bcd48e5a7 | ||
|  | 0e12c0ee4e | ||
| 3caee98cdb | |||
| db7c9bf56d | |||
| 0a332ec501 | |||
| 90367fe53e | |||
| 50c006546c | |||
| e8c171a82e | |||
| 58a60f2c81 | |||
| 0820e1b9f2 | |||
| 46edf2b819 | |||
| 11eb61ba35 | |||
| 813b92af78 | |||
| 5579991d66 | |||
| f4f4fdd3ac | |||
| be215d3f75 | |||
| 235af3519d | |||
| 1292851226 | |||
| dfd0a0994b | |||
| 97fcdda12f | |||
| 055c2db790 | |||
| ecfdab47a8 | |||
| fda68231ea | |||
| d08afff443 | |||
| 17ba6a06ba | |||
| dbd59a4023 | |||
| 5534706189 | |||
| 5428626c3f | |||
| 9b33117cb1 | |||
| 70b2fa4ac2 | |||
| fd6ff86579 | |||
| 549782f230 | |||
| 8cf4848b45 | |||
| 9f6c0a1111 | |||
|  | 1ab49647f6 | ||
|  | 8d4d8a48e0 | ||
| 7aea2af9a1 | |||
| 5cd791aae7 | |||
| df5e5f1a4b | |||
| 64c1fe5276 | |||
| f38ea59569 | |||
| 6cc285d1e8 | |||
| 0d7f7aaca1 | |||
| f8ea2daa73 | |||
|  | c820e89cb7 | ||
| e05f8e7291 | |||
| 2383e8ead3 | |||
| 1a4ee0d3e1 | |||
| 6f3b30e2fe | |||
| b1e4b63c6a | |||
| 3129e8210e | |||
| 5d8de80a18 | |||
|  | 604c16ec90 | ||
|  | a3e821fb16 | ||
|  | 890f50704f | ||
|  | 3416fdf50c | ||
| 43892f0d8c | |||
| 8e6b1e6f52 | |||
|  | ff943a125c | ||
| 9cbbdff993 | |||
| 316358765d | |||
| 14737b5f12 | |||
| caff27b43d | |||
| 589a94216b | |||
| a659fb06fa | |||
| f733fe5636 | |||
| 18423fcd9c | |||
| 72a3681767 | |||
| 160ccbb220 | |||
| 22f96363ba | |||
| 3e7f90733e | |||
| 321f038c7c | |||
| 7a9de79aae | |||
| 862830e9de | |||
| bc0e536d3d | |||
| 82884a75a3 | |||
| 93c44861e0 | |||
| 4ecfc9de38 | |||
| a84c3e0852 | |||
| 8efed966a0 | |||
| e7c4b5bca7 | |||
| ed632248c5 | |||
| 5c202741d6 | |||
| c5ebdbf85b | |||
| c4282a870e | |||
| 91cd6077ce | |||
| c0ed6fa9c8 | |||
| 22c9c3e0f0 | |||
| a709d24520 | |||
| e56138e416 | |||
|  | d516584d90 | ||
| b420a9bba0 | |||
| a20ab79355 | |||
| e0ec15b272 | |||
| 9e998f9a29 | |||
| 1c7c0af712 | |||
| d40a4a171f | |||
| ba430a5849 | |||
| 0f21b01813 | |||
| 10586baf0d | |||
| e979742424 | |||
| ff3f049226 | |||
| 13b45c49da | |||
| a72fb08dc8 | |||
| 944fa89b3c | |||
| 48e1b02e0e | |||
| 3e0bbe5ffe | |||
| fd586d09c3 | |||
|  | c2a4efde2b | ||
| b615c27cf6 | |||
| 22e870b3e5 | |||
| da462346f9 | |||
|  | 406bd84875 | ||
|  | c9deca1180 | ||
| 419cd185fc | |||
| 7976e237ca | |||
| 19ec8f0817 | |||
| ef3a3d6717 | |||
| c2019267d1 | |||
| 0091fbcabb | |||
|  | 6d4f3109f8 | ||
|  | 35cf3a1cd1 | ||
|  | 1a782c1db9 | ||
|  | 3d2ca606ca | ||
| aa7497573e | |||
| 54c1fe51ef | |||
| 7a86b2ad3b | |||
| 6b23c284e5 | |||
| aab6660edd | |||
| c46f41e25f | |||
| 00c0445eed | |||
| c8c6b05b8a | |||
| 08ff1056d7 | |||
| 0f4a7e900f | |||
| 2d6c434bde | |||
| f5d2644031 | |||
| 09b4fa2860 | |||
| a748c1b6b2 | |||
| a17cf78d29 | |||
| 9847433e0a | |||
| 7ce50398d1 | |||
| e30bc177e9 | |||
| b9ff467c0c | |||
| 1174500e3f | |||
| 07df450b3c | |||
| b8d938d3aa | |||
| d1e9340ea9 | |||
| 8925e26c4c | |||
| 9b89462797 | |||
| 51bad64a51 | |||
| 7880294c6f | |||
| 451b15fb6b | |||
| d20700ca74 | |||
| 9cf1cbe425 | |||
| 212cd48249 | |||
| 7adc0c9c08 | |||
| 0a19a6c48b | |||
| 66b5843b0d | |||
| df89fe69e1 | |||
| 9498242901 | |||
| eab6dbd74c | |||
| 6159686a8e | |||
| 1d82bc2261 | |||
| bfcdd79f33 | |||
| d49b68597c | |||
| 1d9f4fc13e | |||
| 6fa0d8e2ff | |||
| 584d02db29 | |||
| 5937c6bf5c | |||
|  | 10b3083647 | ||
|  | bb1a846cb3 | ||
| fc0043e3f6 | |||
| d41ba79ae4 | |||
| f36847d966 | |||
|  | 967e53d83b | ||
|  | d246cdee51 | ||
|  | d518a76536 | ||
|  | 6f65c33be4 | 
							
								
								
									
										14
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | /** | ||||||
|  | !/cmd/africastalking | ||||||
|  | !/cmd/ssh | ||||||
|  | !/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 | AT_ENDPOINT=/ussd/africastalking | ||||||
| 
 | 
 | ||||||
| #PostgreSQL | #PostgreSQL | ||||||
| DB_HOST=localhost | DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd | ||||||
| DB_USER=postgres | #DB_TIMEZONE=Africa/Nairobi | ||||||
| DB_PASSWORD=strongpass | #DB_SCHEMA=vise | ||||||
| DB_NAME=urdt_ussd |  | ||||||
| DB_PORT=5432 |  | ||||||
| DB_SSLMODE=disable |  | ||||||
| DB_TIMEZONE=Africa/Nairobi |  | ||||||
| 
 | 
 | ||||||
| #External API Calls | #External API Calls | ||||||
| CUSTODIAL_URL_BASE=http://localhost:5003 | CUSTODIAL_URL_BASE=http://localhost:5003 | ||||||
| BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr | BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr | ||||||
| DATA_URL_BASE=http://localhost:5006 | DATA_URL_BASE=http://localhost:5006 | ||||||
|  | 
 | ||||||
|  | #Language | ||||||
|  | DEFAULT_LANGUAGE=eng | ||||||
|  | LANGUAGES=eng, swa | ||||||
|  | |||||||
							
								
								
									
										56
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | |||||||
|  | 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 }} | ||||||
							
								
								
									
										44
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | FROM golang:1.23.0-bookworm AS build | ||||||
|  | 
 | ||||||
|  | ENV CGO_ENABLED=1 | ||||||
|  | 
 | ||||||
|  | ARG BUILDPLATFORM | ||||||
|  | ARG TARGETPLATFORM | ||||||
|  | ARG BUILD=dev | ||||||
|  | 
 | ||||||
|  | WORKDIR /build | ||||||
|  | COPY . . | ||||||
|  | RUN apt update && apt install libgdbm-dev | ||||||
|  | RUN git clone https://git.defalsify.org/vise.git go-vise | ||||||
|  | 
 | ||||||
|  | WORKDIR /build/services/registration | ||||||
|  | RUN echo "Compiling go-vise files" | ||||||
|  | RUN make VISE_PATH=/build/go-vise -B | ||||||
|  | 
 | ||||||
|  | WORKDIR /build | ||||||
|  | RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM" | ||||||
|  | RUN go mod download | ||||||
|  | RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go | ||||||
|  | RUN go build -tags logtrace -o ussd-ssh -ldflags="-X main.build=${BUILD} -s -w" cmd/ssh/main.go | ||||||
|  | 
 | ||||||
|  | FROM debian:bookworm-slim | ||||||
|  | 
 | ||||||
|  | ENV DEBIAN_FRONTEND=noninteractive | ||||||
|  | 
 | ||||||
|  | RUN apt update && apt install libgdbm-dev ca-certificates -y | ||||||
|  | RUN apt-get clean && rm -rf /var/lib/apt/lists/* | ||||||
|  | 
 | ||||||
|  | WORKDIR /service | ||||||
|  | 
 | ||||||
|  | COPY --from=build /build/ussd-africastalking . | ||||||
|  | COPY --from=build /build/ussd-ssh . | ||||||
|  | COPY --from=build /build/LICENSE . | ||||||
|  | COPY --from=build /build/README.md . | ||||||
|  | COPY --from=build /build/services ./services | ||||||
|  | COPY --from=build /build/.env.example . | ||||||
|  | RUN mv .env.example .env | ||||||
|  | 
 | ||||||
|  | EXPOSE 7123 | ||||||
|  | EXPOSE 7122 | ||||||
|  | 
 | ||||||
|  | CMD ["./ussd-africastalking"] | ||||||
| @ -1,144 +1,103 @@ | |||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/json" |  | ||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" |  | ||||||
| 	"log" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/signal" | 	"os/signal" | ||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" |  | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 
 | 
 | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"git.defalsify.org/vise.git/engine" | ||||||
|  | 	"git.defalsify.org/vise.git/lang" | ||||||
| 	"git.defalsify.org/vise.git/logging" | 	"git.defalsify.org/vise.git/logging" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/config" | 	"git.grassecon.net/urdt/ussd/config" | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/args" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | 	"git.grassecon.net/urdt/ussd/internal/http/at" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" | 	"git.grassecon.net/urdt/ussd/remote" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	logg        = logging.NewVanilla() | 	logg          = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id") | ||||||
| 	scriptDir     = path.Join("services", "registration") | 	scriptDir     = path.Join("services", "registration") | ||||||
|  | 	build         = "dev" | ||||||
|  | 	menuSeparator = ": " | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	initializers.LoadEnvVariables() | 	initializers.LoadEnvVariables() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type atRequestParser struct{} |  | ||||||
| 
 |  | ||||||
| func (arp *atRequestParser) GetSessionId(rq any) (string, error) { |  | ||||||
| 	rqv, ok := rq.(*http.Request) |  | ||||||
| 	if !ok { |  | ||||||
| 		log.Println("got an invalid request:", rq) |  | ||||||
| 		return "", handlers.ErrInvalidRequest |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Capture body (if any) for logging
 |  | ||||||
| 	body, err := io.ReadAll(rqv.Body) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Println("failed to read request body:", 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 { |  | ||||||
| 		log.Println("failed to marshal request body:", err) |  | ||||||
| 	} else { |  | ||||||
| 		log.Println("Received request:", string(logBytes)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err := rqv.ParseForm(); err != nil { |  | ||||||
| 		log.Println("failed to parse form data: %v", err) |  | ||||||
| 		return "", fmt.Errorf("failed to parse form data: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	phoneNumber := rqv.FormValue("phoneNumber") |  | ||||||
| 	if phoneNumber == "" { |  | ||||||
| 		return "", fmt.Errorf("no phone number found") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return phoneNumber, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (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 main() { | func main() { | ||||||
| 	config.LoadConfig() | 	config.LoadConfig() | ||||||
| 
 | 
 | ||||||
| 	var dbDir string | 	var connStr string | ||||||
| 	var resourceDir string | 	var resourceDir string | ||||||
| 	var size uint | 	var size uint | ||||||
| 	var database string |  | ||||||
| 	var engineDebug bool | 	var engineDebug bool | ||||||
| 	var host string | 	var host string | ||||||
| 	var port uint | 	var port uint | ||||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | 	var err error | ||||||
|  | 	var gettextDir string | ||||||
|  | 	var langs args.LangVar | ||||||
|  | 
 | ||||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | 	flag.StringVar(&connStr, "c", "", "connection string") | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | 	flag.UintVar(&size, "s", 160, "max size of output") | ||||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | ||||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | ||||||
|  | 	flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") | ||||||
|  | 	flag.Var(&langs, "language", "add symbol resolution for language") | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) | 	if connStr == "" { | ||||||
|  | 		connStr = config.DbConn | ||||||
|  | 	} | ||||||
|  | 	connData, err := storage.ToConnData(connStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	logg.Infof("start command", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size) | ||||||
| 
 | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	ctx = context.WithValue(ctx, "Database", database) | 	ln, err := lang.LanguageFromCode(config.DefaultLanguage) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "default language set error: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	ctx = context.WithValue(ctx, "Language", ln) | ||||||
|  | 
 | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
| 
 | 
 | ||||||
| 	cfg := engine.Config{ | 	cfg := engine.Config{ | ||||||
| 		Root:          "root", | 		Root:          "root", | ||||||
| 		OutputSize:    uint32(size), | 		OutputSize:    uint32(size), | ||||||
| 		FlagCount:     uint32(128), | 		FlagCount:     uint32(128), | ||||||
|  | 		MenuSeparator: menuSeparator, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if engineDebug { | 	if engineDebug { | ||||||
| 		cfg.EngineDebug = true | 		cfg.EngineDebug = true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = menuStorageService.EnsureDbDir() | 	rs, err := menuStorageService.GetResource(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| @ -157,6 +116,10 @@ func main() { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | 	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) | 	lhs.SetDataStore(&userdataStore) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -178,9 +141,9 @@ func main() { | |||||||
| 	} | 	} | ||||||
| 	defer stateStore.Close() | 	defer stateStore.Close() | ||||||
| 
 | 
 | ||||||
| 	rp := &atRequestParser{} | 	rp := &at.ATRequestParser{} | ||||||
| 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | ||||||
| 	sh := httpserver.NewATSessionHandler(bsh) | 	sh := at.NewATSessionHandler(bsh) | ||||||
| 
 | 
 | ||||||
| 	mux := http.NewServeMux() | 	mux := http.NewServeMux() | ||||||
| 	mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) | 	mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) | ||||||
|  | |||||||
| @ -10,11 +10,13 @@ import ( | |||||||
| 	"syscall" | 	"syscall" | ||||||
| 
 | 
 | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"git.defalsify.org/vise.git/engine" | ||||||
|  | 	"git.defalsify.org/vise.git/lang" | ||||||
| 	"git.defalsify.org/vise.git/logging" | 	"git.defalsify.org/vise.git/logging" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/config" | 	"git.grassecon.net/urdt/ussd/config" | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/args" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" | 	"git.grassecon.net/urdt/ussd/remote" | ||||||
| @ -23,6 +25,7 @@ import ( | |||||||
| var ( | var ( | ||||||
| 	logg          = logging.NewVanilla() | 	logg          = logging.NewVanilla() | ||||||
| 	scriptDir     = path.Join("services", "registration") | 	scriptDir     = path.Join("services", "registration") | ||||||
|  | 	menuSeparator = ": " | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| @ -34,7 +37,7 @@ type asyncRequestParser struct { | |||||||
| 	input     []byte | 	input     []byte | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *asyncRequestParser) GetSessionId(r any) (string, error) { | func (p *asyncRequestParser) GetSessionId(ctx context.Context, r any) (string, error) { | ||||||
| 	return p.sessionId, nil | 	return p.sessionId, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -45,48 +48,68 @@ func (p *asyncRequestParser) GetInput(r any) ([]byte, error) { | |||||||
| func main() { | func main() { | ||||||
| 	config.LoadConfig() | 	config.LoadConfig() | ||||||
| 
 | 
 | ||||||
|  | 	var connStr string | ||||||
| 	var sessionId string | 	var sessionId string | ||||||
| 	var dbDir string |  | ||||||
| 	var resourceDir string | 	var resourceDir string | ||||||
| 	var size uint | 	var size uint | ||||||
| 	var database string |  | ||||||
| 	var engineDebug bool | 	var engineDebug bool | ||||||
| 	var host string | 	var host string | ||||||
| 	var port uint | 	var port uint | ||||||
|  | 	var err error | ||||||
|  | 	var gettextDir string | ||||||
|  | 	var langs args.LangVar | ||||||
|  | 
 | ||||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") |  | ||||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | 	flag.StringVar(&connStr, "c", "", "connection string") | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | 	flag.UintVar(&size, "s", 160, "max size of output") | ||||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | ||||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | ||||||
|  | 	flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") | ||||||
|  | 	flag.Var(&langs, "language", "add symbol resolution for language") | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) | 	if connStr == "" { | ||||||
|  | 		connStr = config.DbConn | ||||||
|  | 	} | ||||||
|  | 	connData, err := storage.ToConnData(connStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) | ||||||
| 
 | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	ctx = context.WithValue(ctx, "Database", database) | 
 | ||||||
|  | 	ln, err := lang.LanguageFromCode(config.DefaultLanguage) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "default language set error: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	ctx = context.WithValue(ctx, "Language", ln) | ||||||
|  | 
 | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
| 
 | 
 | ||||||
| 	cfg := engine.Config{ | 	cfg := engine.Config{ | ||||||
| 		Root:          "root", | 		Root:          "root", | ||||||
| 		OutputSize:    uint32(size), | 		OutputSize:    uint32(size), | ||||||
| 		FlagCount:     uint32(128), | 		FlagCount:     uint32(128), | ||||||
|  | 		MenuSeparator: menuSeparator, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if engineDebug { | 	if engineDebug { | ||||||
| 		cfg.EngineDebug = true | 		cfg.EngineDebug = true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	err = menuStorageService.EnsureDbDir() | 	rs, err := menuStorageService.GetResource(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
|  | |||||||
| @ -12,11 +12,13 @@ import ( | |||||||
| 	"syscall" | 	"syscall" | ||||||
| 
 | 
 | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"git.defalsify.org/vise.git/engine" | ||||||
|  | 	"git.defalsify.org/vise.git/lang" | ||||||
| 	"git.defalsify.org/vise.git/logging" | 	"git.defalsify.org/vise.git/logging" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/config" | 	"git.grassecon.net/urdt/ussd/config" | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/args" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
| @ -26,6 +28,7 @@ import ( | |||||||
| var ( | var ( | ||||||
| 	logg          = logging.NewVanilla() | 	logg          = logging.NewVanilla() | ||||||
| 	scriptDir     = path.Join("services", "registration") | 	scriptDir     = path.Join("services", "registration") | ||||||
|  | 	menuSeparator = ": " | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| @ -35,46 +38,62 @@ func init() { | |||||||
| func main() { | func main() { | ||||||
| 	config.LoadConfig() | 	config.LoadConfig() | ||||||
| 
 | 
 | ||||||
| 	var dbDir string | 	var connStr string | ||||||
| 	var resourceDir string | 	var resourceDir string | ||||||
| 	var size uint | 	var size uint | ||||||
| 	var database string |  | ||||||
| 	var engineDebug bool | 	var engineDebug bool | ||||||
| 	var host string | 	var host string | ||||||
| 	var port uint | 	var port uint | ||||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | 	var err error | ||||||
|  | 	var gettextDir string | ||||||
|  | 	var langs args.LangVar | ||||||
|  | 
 | ||||||
| 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | 	flag.StringVar(&connStr, "c", "", "connection string") | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | 	flag.UintVar(&size, "s", 160, "max size of output") | ||||||
| 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | 	flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") | ||||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | ||||||
|  | 	flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") | ||||||
|  | 	flag.Var(&langs, "language", "add symbol resolution for language") | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) | 	if connStr == "" { | ||||||
|  | 		connStr = config.DbConn | ||||||
|  | 	} | ||||||
|  | 	connData, err := storage.ToConnData(connStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size) | ||||||
| 
 | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	ctx = context.WithValue(ctx, "Database", database) | 
 | ||||||
|  | 	ln, err := lang.LanguageFromCode(config.DefaultLanguage) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "default language set error: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	ctx = context.WithValue(ctx, "Language", ln) | ||||||
|  | 
 | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
| 
 | 
 | ||||||
| 	cfg := engine.Config{ | 	cfg := engine.Config{ | ||||||
| 		Root:          "root", | 		Root:          "root", | ||||||
| 		OutputSize:    uint32(size), | 		OutputSize:    uint32(size), | ||||||
| 		FlagCount:     uint32(128), | 		FlagCount:     uint32(128), | ||||||
|  | 		MenuSeparator: menuSeparator, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if engineDebug { | 	if engineDebug { | ||||||
| 		cfg.EngineDebug = true | 		cfg.EngineDebug = true | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) | 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	err = menuStorageService.EnsureDbDir() | 	rs, err := menuStorageService.GetResource(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
|  | |||||||
							
								
								
									
										52
									
								
								cmd/main.go
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								cmd/main.go
									
									
									
									
									
								
							| @ -8,10 +8,12 @@ import ( | |||||||
| 	"path" | 	"path" | ||||||
| 
 | 
 | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"git.defalsify.org/vise.git/engine" | ||||||
|  | 	"git.defalsify.org/vise.git/lang" | ||||||
| 	"git.defalsify.org/vise.git/logging" | 	"git.defalsify.org/vise.git/logging" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 	"git.grassecon.net/urdt/ussd/config" | 	"git.grassecon.net/urdt/ussd/config" | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/args" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
| 	"git.grassecon.net/urdt/ussd/remote" | 	"git.grassecon.net/urdt/ussd/remote" | ||||||
| @ -20,32 +22,60 @@ import ( | |||||||
| var ( | var ( | ||||||
| 	logg          = logging.NewVanilla() | 	logg          = logging.NewVanilla() | ||||||
| 	scriptDir     = path.Join("services", "registration") | 	scriptDir     = path.Join("services", "registration") | ||||||
|  | 	menuSeparator = ": " | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	initializers.LoadEnvVariables() | 	initializers.LoadEnvVariables() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility
 | ||||||
| func main() { | func main() { | ||||||
| 	config.LoadConfig() | 	config.LoadConfig() | ||||||
| 
 | 
 | ||||||
| 	var dbDir string | 	var connStr string | ||||||
| 	var size uint | 	var size uint | ||||||
| 	var sessionId string | 	var sessionId string | ||||||
| 	var database string |  | ||||||
| 	var engineDebug bool | 	var engineDebug bool | ||||||
|  | 	var resourceDir string | ||||||
|  | 	var err error | ||||||
|  | 	var gettextDir string | ||||||
|  | 	var langs args.LangVar | ||||||
|  | 
 | ||||||
|  | 	flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir") | ||||||
| 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||||
| 	flag.StringVar(&database, "db", "gdbm", "database to be used") | 	flag.StringVar(&connStr, "c", "", "connection string") | ||||||
| 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") |  | ||||||
| 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||||
| 	flag.UintVar(&size, "s", 160, "max size of output") | 	flag.UintVar(&size, "s", 160, "max size of output") | ||||||
|  | 	flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") | ||||||
|  | 	flag.Var(&langs, "language", "add symbol resolution for language") | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	logg.Infof("start command", "dbdir", dbDir, "outputsize", size) | 	if connStr == "" { | ||||||
|  | 		connStr = config.DbConn | ||||||
|  | 	} | ||||||
|  | 	connData, err := storage.ToConnData(connStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	logg.Infof("start command", "conn", connData, "outputsize", size) | ||||||
|  | 
 | ||||||
|  | 	if len(langs.Langs()) == 0 { | ||||||
|  | 		langs.Set(config.DefaultLanguage) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
| 	ctx = context.WithValue(ctx, "Database", database) | 
 | ||||||
|  | 	ln, err := lang.LanguageFromCode(config.DefaultLanguage) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "default language set error: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	ctx = context.WithValue(ctx, "Language", ln) | ||||||
|  | 
 | ||||||
| 	pfp := path.Join(scriptDir, "pp.csv") | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
| 
 | 
 | ||||||
| 	cfg := engine.Config{ | 	cfg := engine.Config{ | ||||||
| @ -53,15 +83,13 @@ func main() { | |||||||
| 		SessionId:     sessionId, | 		SessionId:     sessionId, | ||||||
| 		OutputSize:    uint32(size), | 		OutputSize:    uint32(size), | ||||||
| 		FlagCount:     uint32(128), | 		FlagCount:     uint32(128), | ||||||
|  | 		MenuSeparator: menuSeparator, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	resourceDir := scriptDir | 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||||
| 	menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) |  | ||||||
| 
 | 
 | ||||||
| 	err := menuStorageService.EnsureDbDir() | 	if gettextDir != "" { | ||||||
| 	if err != nil { | 		menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs()) | ||||||
| 		fmt.Fprintf(os.Stderr, err.Error()) |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	rs, err := menuStorageService.GetResource(ctx) | 	rs, err := menuStorageService.GetResource(ctx) | ||||||
|  | |||||||
							
								
								
									
										34
									
								
								cmd/ssh/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								cmd/ssh/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | # URDT-USSD SSH server | ||||||
|  | 
 | ||||||
|  | An SSH server entry point for the vise engine. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Adding public keys for access | ||||||
|  | 
 | ||||||
|  | Map your (client) public key to a session identifier (e.g. phone number) | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i <session_id> [--dbdir <dbpath>] <client_publickey_filepath> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Create a private key for the server | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | ssh-keygen -N "" -f <server_privatekey_filepath> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Run the server | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | go run -v -tags logtrace ./cmd/ssh/main.go -h <host> -p <port> [--dbdir <dbpath>] <server_privatekey_filepath> | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## Connect to the server | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | ssh [-v] -T -p <port> -i <client_publickey_filepath> <host> | ||||||
|  | ``` | ||||||
							
								
								
									
										144
									
								
								cmd/ssh/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								cmd/ssh/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,144 @@ | |||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"flag" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"os/signal" | ||||||
|  | 	"path" | ||||||
|  | 	"sync" | ||||||
|  | 	"syscall" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/db" | ||||||
|  | 	"git.defalsify.org/vise.git/engine" | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
|  | 
 | ||||||
|  | 	"git.grassecon.net/urdt/ussd/config" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/ssh" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	wg        sync.WaitGroup | ||||||
|  | 	keyStore  db.Db | ||||||
|  | 	logg      = logging.NewVanilla() | ||||||
|  | 	scriptDir = path.Join("services", "registration") | ||||||
|  | 
 | ||||||
|  | 	build = "dev" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	initializers.LoadEnvVariables() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	config.LoadConfig() | ||||||
|  | 
 | ||||||
|  | 	var connStr string | ||||||
|  | 	var authConnStr string | ||||||
|  | 	var resourceDir string | ||||||
|  | 	var size uint | ||||||
|  | 	var engineDebug bool | ||||||
|  | 	var stateDebug bool | ||||||
|  | 	var host string | ||||||
|  | 	var port uint | ||||||
|  | 	flag.StringVar(&connStr, "c", "", "connection string") | ||||||
|  | 	flag.StringVar(&authConnStr, "authdb", "", "auth connection string") | ||||||
|  | 	flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") | ||||||
|  | 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||||
|  | 	flag.UintVar(&size, "s", 160, "max size of output") | ||||||
|  | 	flag.StringVar(&host, "h", "127.0.0.1", "socket host") | ||||||
|  | 	flag.UintVar(&port, "p", 7122, "socket port") | ||||||
|  | 	flag.Parse() | ||||||
|  | 
 | ||||||
|  | 	if connStr == "" { | ||||||
|  | 		connStr = config.DbConn | ||||||
|  | 	} | ||||||
|  | 	if authConnStr == "" { | ||||||
|  | 		authConnStr = connStr | ||||||
|  | 	} | ||||||
|  | 	connData, err := storage.ToConnData(connStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	authConnData, err := storage.ToConnData(authConnStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "auth connstr err: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	sshKeyFile := flag.Arg(0) | ||||||
|  | 	_, err = os.Stat(sshKeyFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING") | ||||||
|  | 	logg.WarnCtxf(ctx, "!!!!! =======================") | ||||||
|  | 	logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!") | ||||||
|  | 	logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!") | ||||||
|  | 	logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)") | ||||||
|  | 
 | ||||||
|  | 	logg.Infof("start command", "conn", connData, "authconn", authConnData, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port) | ||||||
|  | 
 | ||||||
|  | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
|  | 
 | ||||||
|  | 	cfg := engine.Config{ | ||||||
|  | 		Root:       "root", | ||||||
|  | 		OutputSize: uint32(size), | ||||||
|  | 		FlagCount:  uint32(16), | ||||||
|  | 	} | ||||||
|  | 	if stateDebug { | ||||||
|  | 		cfg.StateDebug = true | ||||||
|  | 	} | ||||||
|  | 	if engineDebug { | ||||||
|  | 		cfg.EngineDebug = true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	authKeyStore, err := ssh.NewSshKeyStore(ctx, authConnData.String()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "keystore file open error: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	defer func() { | ||||||
|  | 		logg.TraceCtxf(ctx, "shutdown auth key store reached") | ||||||
|  | 		err = authKeyStore.Close() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logg.ErrorCtxf(ctx, "keystore close error", "err", err) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	cint := make(chan os.Signal) | ||||||
|  | 	cterm := make(chan os.Signal) | ||||||
|  | 	signal.Notify(cint, os.Interrupt, syscall.SIGINT) | ||||||
|  | 	signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) | ||||||
|  | 
 | ||||||
|  | 	runner := &ssh.SshRunner{ | ||||||
|  | 		Cfg: cfg, | ||||||
|  | 		Debug: engineDebug, | ||||||
|  | 		FlagFile: pfp, | ||||||
|  | 		Conn: connData, | ||||||
|  | 		ResourceDir: resourceDir, | ||||||
|  | 		SrvKeyFile:  sshKeyFile, | ||||||
|  | 		Host:        host, | ||||||
|  | 		Port:        port, | ||||||
|  | 	} | ||||||
|  | 	go func() { | ||||||
|  | 		select { | ||||||
|  | 		case _ = <-cint: | ||||||
|  | 		case _ = <-cterm: | ||||||
|  | 		} | ||||||
|  | 		logg.TraceCtxf(ctx, "shutdown runner reached") | ||||||
|  | 		err := runner.Stop() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logg.ErrorCtxf(ctx, "runner stop error", "err", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 	}() | ||||||
|  | 	runner.Run(ctx, authKeyStore) | ||||||
|  | } | ||||||
							
								
								
									
										44
									
								
								cmd/ssh/sshkey/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								cmd/ssh/sshkey/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"flag" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 
 | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/ssh" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	var dbDir string | ||||||
|  | 	var sessionId string | ||||||
|  | 	flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") | ||||||
|  | 	flag.StringVar(&sessionId, "i", "", "session id") | ||||||
|  | 	flag.Parse() | ||||||
|  | 
 | ||||||
|  | 	if sessionId == "" { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "empty session id\n") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 
 | ||||||
|  | 	sshKeyFile := flag.Arg(0) | ||||||
|  | 	if sshKeyFile == "" { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "missing key file argument\n") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	store, err := ssh.NewSshKeyStore(ctx, dbDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "%v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	defer store.Close() | ||||||
|  | 
 | ||||||
|  | 	err = store.AddFromFile(ctx, sshKeyFile, sessionId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "%v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										75
									
								
								common/db.go
									
									
									
									
									
								
							
							
						
						
									
										75
									
								
								common/db.go
									
									
									
									
									
								
							| @ -7,32 +7,88 @@ import ( | |||||||
| 	"git.defalsify.org/vise.git/logging" | 	"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 | type DataTyp uint16 | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	DATA_ACCOUNT DataTyp = iota | 	// API Tracking id to follow status of account creation
 | ||||||
| 	DATA_ACCOUNT_CREATED | 	DATA_TRACKING_ID = iota | ||||||
| 	DATA_TRACKING_ID | 	// EVM address returned from API on account creation
 | ||||||
| 	DATA_PUBLIC_KEY | 	DATA_PUBLIC_KEY | ||||||
| 	DATA_CUSTODIAL_ID | 	// Currently active PIN used to authenticate ussd state change requests
 | ||||||
| 	DATA_ACCOUNT_PIN | 	DATA_ACCOUNT_PIN | ||||||
| 	DATA_ACCOUNT_STATUS | 	// The first name of the user
 | ||||||
| 	DATA_FIRST_NAME | 	DATA_FIRST_NAME | ||||||
|  | 	// The last name of the user
 | ||||||
| 	DATA_FAMILY_NAME | 	DATA_FAMILY_NAME | ||||||
|  | 	// The year-of-birth of the user
 | ||||||
| 	DATA_YOB | 	DATA_YOB | ||||||
|  | 	// The location of the user
 | ||||||
| 	DATA_LOCATION | 	DATA_LOCATION | ||||||
|  | 	// The gender of the user
 | ||||||
| 	DATA_GENDER | 	DATA_GENDER | ||||||
|  | 	// The offerings description of the user
 | ||||||
| 	DATA_OFFERINGS | 	DATA_OFFERINGS | ||||||
|  | 	// The ethereum address of the recipient of an ongoing send request
 | ||||||
| 	DATA_RECIPIENT | 	DATA_RECIPIENT | ||||||
|  | 	// The voucher value amount of an ongoing send request
 | ||||||
| 	DATA_AMOUNT | 	DATA_AMOUNT | ||||||
|  | 	// A general swap field for temporary values
 | ||||||
| 	DATA_TEMPORARY_VALUE | 	DATA_TEMPORARY_VALUE | ||||||
|  | 	// Currently active voucher symbol of user
 | ||||||
| 	DATA_ACTIVE_SYM | 	DATA_ACTIVE_SYM | ||||||
|  | 	// Voucher balance of user's currently active voucher
 | ||||||
| 	DATA_ACTIVE_BAL | 	DATA_ACTIVE_BAL | ||||||
|  | 	// String boolean indicating whether use of PIN is blocked
 | ||||||
| 	DATA_BLOCKED_NUMBER | 	DATA_BLOCKED_NUMBER | ||||||
|  | 	// Reverse mapping of a user's evm address to a session id.
 | ||||||
| 	DATA_PUBLIC_KEY_REVERSE | 	DATA_PUBLIC_KEY_REVERSE | ||||||
|  | 	// Decimal count of the currently active voucher
 | ||||||
| 	DATA_ACTIVE_DECIMAL | 	DATA_ACTIVE_DECIMAL | ||||||
|  | 	// EVM address of the currently active voucher
 | ||||||
| 	DATA_ACTIVE_ADDRESS | 	DATA_ACTIVE_ADDRESS | ||||||
| 	DATA_TRANSACTIONS | 	//Holds count of the number of incorrect PIN attempts
 | ||||||
|  | 	DATA_INCORRECT_PIN_ATTEMPTS | ||||||
|  | 	//ISO 639 code for the selected language.
 | ||||||
|  | 	DATA_SELECTED_LANGUAGE_CODE | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// List of valid voucher symbols in the user context.
 | ||||||
|  | 	DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota | ||||||
|  | 	// List of voucher balances for vouchers valid in the user context.
 | ||||||
|  | 	DATA_VOUCHER_BALANCES | ||||||
|  | 	// List of voucher decimal counts for vouchers valid in the user context.
 | ||||||
|  | 	DATA_VOUCHER_DECIMALS | ||||||
|  | 	// List of voucher EVM addresses for vouchers valid in the user context.
 | ||||||
|  | 	DATA_VOUCHER_ADDRESSES | ||||||
|  | 	// List of senders for valid transactions in the user context.
 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	DATA_TX_SENDERS = 512 + iota | ||||||
|  | 	// List of recipients for valid transactions in the user context.
 | ||||||
|  | 	DATA_TX_RECIPIENTS | ||||||
|  | 	// List of voucher values for valid transactions in the user context.
 | ||||||
|  | 	DATA_TX_VALUES | ||||||
|  | 	// List of voucher EVM addresses for valid transactions in the user context.
 | ||||||
|  | 	DATA_TX_ADDRESSES | ||||||
|  | 	// List of valid transaction hashes in the user context.
 | ||||||
|  | 	DATA_TX_HASHES | ||||||
|  | 	// List of transaction dates for valid transactions in the user context.
 | ||||||
|  | 	DATA_TX_DATES | ||||||
|  | 	// List of voucher symbols for valid transactions in the user context.
 | ||||||
|  | 	DATA_TX_SYMBOLS | ||||||
|  | 	// List of voucher decimal counts for valid transactions in the user context.
 | ||||||
|  | 	DATA_TX_DECIMALS | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| @ -69,3 +125,10 @@ func StringToDataTyp(str string) (DataTyp, error) { | |||||||
| 		return 0, errors.New("invalid DataTyp string") | 		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 | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										37
									
								
								common/pin.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								common/pin.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | package common | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"regexp" | ||||||
|  | 
 | ||||||
|  | 	"golang.org/x/crypto/bcrypt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// Define the regex pattern as a constant
 | ||||||
|  | 	pinPattern = `^\d{4}$` | ||||||
|  | 
 | ||||||
|  | 	//Allowed incorrect  PIN attempts
 | ||||||
|  | 	AllowedPINAttempts = uint8(3) | ||||||
|  | 	 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // checks whether the given input is a 4 digit number
 | ||||||
|  | func IsValidPIN(pin string) bool { | ||||||
|  | 	match, _ := regexp.MatchString(pinPattern, pin) | ||||||
|  | 	return match | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // HashPIN uses bcrypt with 8 salt rounds to hash the PIN
 | ||||||
|  | func HashPIN(pin string) (string, error) { | ||||||
|  | 	hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return string(hash), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // VerifyPIN compareS the hashed PIN with the plaintext PIN
 | ||||||
|  | func VerifyPIN(hashedPIN, pin string) bool { | ||||||
|  | 	err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin)) | ||||||
|  | 	return err == nil | ||||||
|  | } | ||||||
							
								
								
									
										173
									
								
								common/pin_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								common/pin_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | |||||||
|  | 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 | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								common/recipient.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								common/recipient.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | |||||||
|  | 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 | ||||||
|  | } | ||||||
| @ -8,31 +8,32 @@ import ( | |||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 	"git.defalsify.org/vise.git/persist" | 	"git.defalsify.org/vise.git/persist" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
|  | 	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func StoreToDb(store *UserDataStore) db.Db { | func StoreToDb(store *UserDataStore) db.Db { | ||||||
| 	return store.Db | 	return store.Db | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb { | func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb { | ||||||
| 	return storage.NewSubPrefixDb(store.Db, pfx)	 | 	return dbstorage.NewSubPrefixDb(store.Db, pfx)	 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type StorageServices interface { | type StorageServices interface { | ||||||
| 	GetPersister(ctx context.Context) (*persist.Persister, error) | 	GetPersister(ctx context.Context) (*persist.Persister, error) | ||||||
| 	GetUserdataDb(ctx context.Context) (db.Db, error) | 	GetUserdataDb(ctx context.Context) (db.Db, error) | ||||||
| 	GetResource(ctx context.Context) (resource.Resource, error) | 	GetResource(ctx context.Context) (resource.Resource, error) | ||||||
| 	EnsureDbDir() error |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type StorageService struct { | type StorageService struct { | ||||||
| 	svc *storage.MenuStorageService | 	svc *storage.MenuStorageService | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewStorageService(dbDir string) *StorageService { | func NewStorageService(conn storage.ConnData) (*StorageService, error) { | ||||||
| 	return &StorageService{ | 	svc := &StorageService{ | ||||||
| 		svc: storage.NewMenuStorageService(dbDir, ""), | 		svc: storage.NewMenuStorageService(conn, ""), | ||||||
| 	} | 	} | ||||||
|  | 	return svc, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { | func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { | ||||||
| @ -46,7 +47,3 @@ func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) { | |||||||
| func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) { | func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) { | ||||||
| 	return nil, errors.New("not implemented") | 	return nil, errors.New("not implemented") | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func(ss *StorageService) EnsureDbDir() error { |  | ||||||
| 	return ss.svc.EnsureDbDir() |  | ||||||
| } |  | ||||||
|  | |||||||
							
								
								
									
										119
									
								
								common/transfer_statements.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								common/transfer_statements.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | |||||||
|  | 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") | ||||||
|  | } | ||||||
| @ -20,7 +20,7 @@ type UserDataStore struct { | |||||||
| func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { | func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { | ||||||
| 	store.SetPrefix(db.DATATYPE_USERDATA) | 	store.SetPrefix(db.DATATYPE_USERDATA) | ||||||
| 	store.SetSession(sessionId) | 	store.SetSession(sessionId) | ||||||
| 	k := PackKey(typ, []byte(sessionId)) | 	k := ToBytes(typ) | ||||||
| 	return store.Get(ctx, k) | 	return store.Get(ctx, k) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -29,6 +29,6 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ | |||||||
| func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { | func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { | ||||||
| 	store.SetPrefix(db.DATATYPE_USERDATA) | 	store.SetPrefix(db.DATATYPE_USERDATA) | ||||||
| 	store.SetSession(sessionId) | 	store.SetSession(sessionId) | ||||||
| 	k := PackKey(typ, []byte(sessionId)) | 	k := ToBytes(typ) | ||||||
| 	return store.Put(ctx, k, value) | 	return store.Put(ctx, k, value) | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ import ( | |||||||
| 	"math/big" | 	"math/big" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db" | ||||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -63,23 +63,24 @@ func ScaleDownBalance(balance, decimals string) string { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetVoucherData retrieves and matches voucher data
 | // GetVoucherData retrieves and matches voucher data
 | ||||||
| func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { | func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { | ||||||
| 	keys := []string{"sym", "bal", "deci", "addr"} | 	keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES} | ||||||
| 	data := make(map[string]string) | 	data := make(map[DataTyp]string) | ||||||
| 
 | 
 | ||||||
| 	for _, key := range keys { | 	for _, key := range keys { | ||||||
| 		value, err := db.Get(ctx, []byte(key)) | 		value, err := db.Get(ctx, ToBytes(key)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("failed to get %s: %v", key, err) | 			return nil, fmt.Errorf("failed to get %s: %v", ToBytes(key), err) | ||||||
| 		} | 		} | ||||||
| 		data[key] = string(value) | 		data[key] = string(value) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	symbol, balance, decimal, address := MatchVoucher(input, | 	symbol, balance, decimal, address := MatchVoucher(input, | ||||||
| 		data["sym"], | 		data[DATA_VOUCHER_SYMBOLS], | ||||||
| 		data["bal"], | 		data[DATA_VOUCHER_BALANCES], | ||||||
| 		data["deci"], | 		data[DATA_VOUCHER_DECIMALS], | ||||||
| 		data["addr"]) | 		data[DATA_VOUCHER_ADDRESSES], | ||||||
|  | 	) | ||||||
| 
 | 
 | ||||||
| 	if symbol == "" { | 	if symbol == "" { | ||||||
| 		return nil, nil | 		return nil, nil | ||||||
| @ -151,7 +152,7 @@ func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId str | |||||||
| 	return data, nil | 	return data, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // UpdateVoucherData sets the active voucher data in the DataStore.
 | // UpdateVoucherData updates the active voucher data in the DataStore.
 | ||||||
| func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { | func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { | ||||||
| 	logg.TraceCtxf(ctx, "dtal", "data", data) | 	logg.TraceCtxf(ctx, "dtal", "data", data) | ||||||
| 	// Active voucher data entries
 | 	// Active voucher data entries
 | ||||||
|  | |||||||
| @ -8,8 +8,9 @@ import ( | |||||||
| 	"github.com/alecthomas/assert/v2" | 	"github.com/alecthomas/assert/v2" | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	visedb "git.defalsify.org/vise.git/db" | ||||||
| 	memdb "git.defalsify.org/vise.git/db/mem" | 	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" | 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -83,19 +84,21 @@ func TestGetVoucherData(t *testing.T) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	spdb := storage.NewSubPrefixDb(db, []byte("vouchers")) | 
 | ||||||
|  | 	prefix := ToBytes(visedb.DATATYPE_USERDATA) | ||||||
|  | 	spdb := dbstorage.NewSubPrefixDb(db, prefix) | ||||||
| 
 | 
 | ||||||
| 	// Test voucher data
 | 	// Test voucher data
 | ||||||
| 	mockData := map[string][]byte{ | 	mockData := map[DataTyp][]byte{ | ||||||
| 		"sym":  []byte("1:SRF\n2:MILO"), | 		DATA_VOUCHER_SYMBOLS:   []byte("1:SRF\n2:MILO"), | ||||||
| 		"bal":  []byte("1:100\n2:200"), | 		DATA_VOUCHER_BALANCES:  []byte("1:100\n2:200"), | ||||||
| 		"deci": []byte("1:6\n2:4"), | 		DATA_VOUCHER_DECIMALS:  []byte("1:6\n2:4"), | ||||||
| 		"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), | 		DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Put the data
 | 	// Put the data
 | ||||||
| 	for key, value := range mockData { | 	for key, value := range mockData { | ||||||
| 		err = spdb.Put(ctx, []byte(key), []byte(value)) | 		err = spdb.Put(ctx, []byte(ToBytes(key)), []byte(value)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatal(err) | 			t.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package config | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
| ) | ) | ||||||
| @ -15,6 +16,12 @@ const ( | |||||||
| 	voucherHoldingsPathPrefix  = "/api/v1/holdings" | 	voucherHoldingsPathPrefix  = "/api/v1/holdings" | ||||||
| 	voucherTransfersPathPrefix = "/api/v1/transfers/last10" | 	voucherTransfersPathPrefix = "/api/v1/transfers/last10" | ||||||
| 	voucherDataPathPrefix      = "/api/v1/token" | 	voucherDataPathPrefix      = "/api/v1/token" | ||||||
|  | 	AliasPrefix                = "api/v1/alias" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	defaultLanguage		   = "eng" | ||||||
|  | 	languages []string | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| @ -32,8 +39,30 @@ var ( | |||||||
| 	VoucherHoldingsURL  string | 	VoucherHoldingsURL  string | ||||||
| 	VoucherTransfersURL string | 	VoucherTransfersURL string | ||||||
| 	VoucherDataURL      string | 	VoucherDataURL      string | ||||||
|  | 	CheckAliasURL       string | ||||||
|  | 	DbConn		string | ||||||
|  | 	DefaultLanguage	    string | ||||||
|  | 	Languages	[]string | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | func setLanguage() error { | ||||||
|  | 	defaultLanguage = initializers.GetEnv("DEFAULT_LANGUAGE", defaultLanguage) | ||||||
|  | 	languages = strings.Split(initializers.GetEnv("LANGUAGES", defaultLanguage), ",") | ||||||
|  | 	haveDefaultLanguage := false | ||||||
|  | 	for i, v := range(languages) { | ||||||
|  | 		languages[i] = strings.ReplaceAll(v, " ", "") | ||||||
|  | 		if languages[i] == defaultLanguage { | ||||||
|  | 			haveDefaultLanguage = true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if !haveDefaultLanguage { | ||||||
|  | 		languages = append([]string{defaultLanguage}, languages...) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func setBase() error { | func setBase() error { | ||||||
| 	var err error | 	var err error | ||||||
| 
 | 
 | ||||||
| @ -41,14 +70,20 @@ func setBase() error { | |||||||
| 	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006") | 	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006") | ||||||
| 	BearerToken = initializers.GetEnv("BEARER_TOKEN", "") | 	BearerToken = initializers.GetEnv("BEARER_TOKEN", "") | ||||||
| 
 | 
 | ||||||
| 	_, err = url.JoinPath(custodialURLBase, "/foo") | 	_, err = url.Parse(custodialURLBase) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	_, err = url.JoinPath(dataURLBase, "/bar") | 	_, err = url.Parse(dataURLBase) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func setConn() error { | ||||||
|  | 	DbConn = initializers.GetEnv("DB_CONN", "") | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -58,6 +93,14 @@ func LoadConfig() error { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 	err = setConn() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	err = setLanguage() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
| 	CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath) | 	CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath) | ||||||
| 	TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath) | 	TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath) | ||||||
| 	BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix) | 	BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix) | ||||||
| @ -66,6 +109,9 @@ func LoadConfig() error { | |||||||
| 	VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix) | 	VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix) | ||||||
| 	VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) | 	VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) | ||||||
| 	VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) | 	VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) | ||||||
|  | 	CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix) | ||||||
|  | 	DefaultLanguage = defaultLanguage | ||||||
|  | 	Languages = languages | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								debug/cap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								debug/cap.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | package debug | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	DebugCap uint32 | ||||||
|  | ) | ||||||
							
								
								
									
										84
									
								
								debug/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								debug/db.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | |||||||
|  | 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))] | ||||||
|  | } | ||||||
							
								
								
									
										44
									
								
								debug/db_debug.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								debug/db_debug.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | // +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" | ||||||
|  | } | ||||||
							
								
								
									
										78
									
								
								debug/db_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								debug/db_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | |||||||
|  | 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) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								dev/dialoguss/sample_user.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								dev/dialoguss/sample_user.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | url: http://localhost:7123 | ||||||
|  | dial: "*384*96#" | ||||||
|  | phoneNumber: +254722123456 | ||||||
							
								
								
									
										21
									
								
								dev/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								dev/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | 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
									
								
								dev/init_db.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								dev/init_db.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | CREATE DATABASE urdt_ussd; | ||||||
							
								
								
									
										126
									
								
								devtools/lang/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								devtools/lang/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | |||||||
|  | // create language files from environment
 | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"flag" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
|  | 	"git.defalsify.org/vise.git/lang" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/config" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	 | ||||||
|  | 	changeHeadSrc = `LOAD reset_account_authorized 0 | ||||||
|  | LOAD reset_incorrect 0 | ||||||
|  | CATCH incorrect_pin flag_incorrect_pin 1 | ||||||
|  | CATCH pin_entry flag_account_authorized 0 | ||||||
|  | `  | ||||||
|  | 
 | ||||||
|  | 	selectSrc = `LOAD set_language 6 | ||||||
|  | RELOAD set_language | ||||||
|  | CATCH terms flag_account_created 0 | ||||||
|  | MOVE language_changed | ||||||
|  | ` | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	logg = logging.NewVanilla() | ||||||
|  | 	mouts string | ||||||
|  | 	incmps string | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	initializers.LoadEnvVariables() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func toLanguageLabel(ln lang.Language) string { | ||||||
|  | 	s := ln.Name | ||||||
|  | 	v := strings.Split(s, " (") | ||||||
|  | 	if len(v) > 1 { | ||||||
|  | 		s = v[0] | ||||||
|  | 	} | ||||||
|  | 	return s | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func toLanguageKey(ln lang.Language) string { | ||||||
|  | 	s := toLanguageLabel(ln) | ||||||
|  | 	return strings.ToLower(s) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	var srcDir string | ||||||
|  | 
 | ||||||
|  | 	flag.StringVar(&srcDir, "o", ".", "resource dir write to") | ||||||
|  | 	flag.Parse() | ||||||
|  | 
 | ||||||
|  | 	logg.Infof("start command", "dir", srcDir) | ||||||
|  | 
 | ||||||
|  | 	err := config.LoadConfig() | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "config load error: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	logg.Tracef("using languages", "lang", config.Languages) | ||||||
|  | 
 | ||||||
|  | 	for i, v := range(config.Languages) { | ||||||
|  | 		ln, err := lang.LanguageFromCode(v) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v) | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 		n := i + 1 | ||||||
|  | 		s := toLanguageKey(ln) | ||||||
|  | 		mouts += fmt.Sprintf("MOUT %s %v\n", s, n) | ||||||
|  | 		v = "set_" + ln.Code | ||||||
|  | 		incmps += fmt.Sprintf("INCMP %s %v\n", v, n) | ||||||
|  | 
 | ||||||
|  | 		p := path.Join(srcDir, v) | ||||||
|  | 		w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err) | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 		s = toLanguageLabel(ln) | ||||||
|  | 		defer w.Close() | ||||||
|  | 		_, err = w.Write([]byte(s)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	src := mouts + "HALT\n" + incmps | ||||||
|  | 	src += "INCMP . *\n" | ||||||
|  | 
 | ||||||
|  | 	p := path.Join(srcDir, "select_language.vis") | ||||||
|  | 	w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	defer w.Close() | ||||||
|  | 	_, err = w.Write([]byte(src)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	src = changeHeadSrc + src | ||||||
|  | 	p = path.Join(srcDir, "change_language.vis") | ||||||
|  | 	w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	defer w.Close() | ||||||
|  | 	_, err = w.Write([]byte(src)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										100
									
								
								devtools/store/dump/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								devtools/store/dump/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | |||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"flag" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 
 | ||||||
|  | 	"git.grassecon.net/urdt/ussd/config" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/debug" | ||||||
|  | 	"git.defalsify.org/vise.git/db" | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	logg      = logging.NewVanilla() | ||||||
|  | 	scriptDir = path.Join("services", "registration") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	initializers.LoadEnvVariables() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | func formatItem(k []byte, v []byte) (string, error) { | ||||||
|  | 	o, err := debug.FromKey(k) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	s := fmt.Sprintf("%vValue: %v\n\n", o, string(v)) | ||||||
|  | 	return s, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	config.LoadConfig() | ||||||
|  | 
 | ||||||
|  | 	var connStr string | ||||||
|  | 	var sessionId string | ||||||
|  | 	var database string | ||||||
|  | 	var engineDebug bool | ||||||
|  | 	var err error | ||||||
|  | 
 | ||||||
|  | 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||||
|  | 	flag.StringVar(&connStr, "c", ".state", "connection string") | ||||||
|  | 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||||
|  | 	flag.Parse() | ||||||
|  | 
 | ||||||
|  | 	if connStr != "" { | ||||||
|  | 		connStr = config.DbConn | ||||||
|  | 	} | ||||||
|  | 	connData, err := storage.ToConnData(config.DbConn) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	logg.Infof("start command", "conn", connData) | ||||||
|  | 
 | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 	ctx = context.WithValue(ctx, "Database", database) | ||||||
|  | 
 | ||||||
|  | 	resourceDir := scriptDir | ||||||
|  | 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||||
|  | 
 | ||||||
|  | 	store, err := menuStorageService.GetUserdataDb(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "get userdata db: %v\n", err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	store.SetPrefix(db.DATATYPE_USERDATA) | ||||||
|  | 
 | ||||||
|  | 	d, err := store.Dump(ctx, []byte(sessionId)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "store dump fail: %v\n", err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for true { | ||||||
|  | 		k, v := d.Next(ctx) | ||||||
|  | 		if k == nil { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		r, err := formatItem(k, v) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Fprintf(os.Stderr, "format db item error: %v", err) | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 		fmt.Printf(r) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = store.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								devtools/store/generate/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								devtools/store/generate/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | |||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/sha1" | ||||||
|  | 	"flag" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/common" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/config" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
|  | 	testdataloader "github.com/peteole/testdata-loader" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	logg      = logging.NewVanilla() | ||||||
|  | 	baseDir   = testdataloader.GetBasePath() | ||||||
|  | 	scriptDir = path.Join("services", "registration") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	initializers.LoadEnvVariables() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	config.LoadConfig() | ||||||
|  | 
 | ||||||
|  | 	var connStr string | ||||||
|  | 	var sessionId string | ||||||
|  | 	var database string | ||||||
|  | 	var engineDebug bool | ||||||
|  | 	var err error | ||||||
|  | 
 | ||||||
|  | 	flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") | ||||||
|  | 	flag.StringVar(&connStr, "c", "", "connection string") | ||||||
|  | 	flag.BoolVar(&engineDebug, "d", false, "use engine debug output") | ||||||
|  | 	flag.Parse() | ||||||
|  | 
 | ||||||
|  | 	if connStr != "" { | ||||||
|  | 		connStr = config.DbConn | ||||||
|  | 	} | ||||||
|  | 	connData, err := storage.ToConnData(config.DbConn) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	logg.Infof("start command", "conn", connData) | ||||||
|  | 
 | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 	ctx = context.WithValue(ctx, "Database", database) | ||||||
|  | 
 | ||||||
|  | 	resourceDir := scriptDir | ||||||
|  | 	menuStorageService := storage.NewMenuStorageService(connData, resourceDir) | ||||||
|  | 	 | ||||||
|  | 	store, err := menuStorageService.GetUserdataDb(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 	userStore := common.UserDataStore{store} | ||||||
|  | 
 | ||||||
|  | 	h := sha1.New() | ||||||
|  | 	h.Write([]byte(sessionId)) | ||||||
|  | 	address := h.Sum(nil) | ||||||
|  | 	addressString := fmt.Sprintf("%x", address) | ||||||
|  | 
 | ||||||
|  | 	err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = store.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										28
									
								
								doc/data.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								doc/data.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | # Internals | ||||||
|  | 
 | ||||||
|  | ## Version | ||||||
|  | 
 | ||||||
|  | This document describes component versions: | ||||||
|  | 
 | ||||||
|  | * `urdt-ussd` `v0.5.0-beta` | ||||||
|  | * `go-vise` `v0.2.2` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ## User profile data | ||||||
|  | 
 | ||||||
|  | All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix. | ||||||
|  | 
 | ||||||
|  | For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | 0x322b3235343132330002 | ||||||
|  | 
 | ||||||
|  | prefix   sessionid       subprefix | ||||||
|  | 32       2b323534313233  0002 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Sub-prefixes | ||||||
|  | 
 | ||||||
|  | All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_` | ||||||
|  | 
 | ||||||
|  | Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item. | ||||||
							
								
								
									
										36
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								go.mod
									
									
									
									
									
								
							| @ -2,29 +2,17 @@ module git.grassecon.net/urdt/ussd | |||||||
| 
 | 
 | ||||||
| go 1.23.0 | go 1.23.0 | ||||||
| 
 | 
 | ||||||
| toolchain go1.23.2 |  | ||||||
| 
 |  | ||||||
| require ( | require ( | ||||||
| 	git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b | 	git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d | ||||||
| 	github.com/alecthomas/assert/v2 v2.2.2 | 	github.com/alecthomas/assert/v2 v2.2.2 | ||||||
|  | 	github.com/gofrs/uuid v4.4.0+incompatible | ||||||
| 	github.com/grassrootseconomics/eth-custodial v1.3.0-beta | 	github.com/grassrootseconomics/eth-custodial v1.3.0-beta | ||||||
| 	github.com/peteole/testdata-loader v0.3.0 | 	github.com/grassrootseconomics/ussd-data-service v1.2.0-beta | ||||||
| 	gopkg.in/leonelquinteros/gotext.v1 v1.3.1 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect |  | ||||||
| 
 |  | ||||||
| require ( |  | ||||||
| 	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/joho/godotenv v1.5.1 | 	github.com/joho/godotenv v1.5.1 | ||||||
| 	github.com/kr/text v0.2.0 // indirect | 	github.com/peteole/testdata-loader v0.3.0 | ||||||
| 	github.com/rogpeppe/go-internal v1.13.1 // indirect | 	github.com/stretchr/testify v1.9.0 | ||||||
| 	golang.org/x/crypto v0.27.0 // indirect | 	golang.org/x/crypto v0.27.0 | ||||||
| 	golang.org/x/sync v0.8.0 // indirect | 	gopkg.in/leonelquinteros/gotext.v1 v1.3.1 | ||||||
| 	golang.org/x/text v0.18.0 // indirect |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| @ -33,13 +21,19 @@ require ( | |||||||
| 	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect | 	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect | ||||||
| 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect | 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect | ||||||
| 	github.com/fxamacker/cbor/v2 v2.4.0 // indirect | 	github.com/fxamacker/cbor/v2 v2.4.0 // indirect | ||||||
| 	github.com/gofrs/uuid v4.4.0+incompatible |  | ||||||
| 	github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect | 	github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect | ||||||
| 	github.com/hexops/gotextdiff v1.0.3 // indirect | 	github.com/hexops/gotextdiff v1.0.3 // indirect | ||||||
|  | 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||||
|  | 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect | ||||||
|  | 	github.com/jackc/pgx/v5 v5.7.1 // indirect | ||||||
|  | 	github.com/jackc/puddle/v2 v2.2.2 // indirect | ||||||
|  | 	github.com/kr/text v0.2.0 // indirect | ||||||
| 	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect | 	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | 	github.com/pmezard/go-difflib v1.0.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/objx v0.5.2 // indirect | ||||||
| 	github.com/stretchr/testify v1.9.0 |  | ||||||
| 	github.com/x448/float16 v0.8.4 // indirect | 	github.com/x448/float16 v0.8.4 // indirect | ||||||
|  | 	golang.org/x/sync v0.8.0 // indirect | ||||||
|  | 	golang.org/x/text v0.18.0 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | |||||||
| git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU= | git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw= | ||||||
| git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= | 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 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= | ||||||
| github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= | github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= | ||||||
| github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= | github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= | ||||||
| @ -18,8 +18,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 | |||||||
| github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= | 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 h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU= | ||||||
| github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= | github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= | ||||||
| github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU= | github.com/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk= | ||||||
| github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw= | github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0= | ||||||
| github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= | github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= | ||||||
| github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= | github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= | ||||||
| github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= | ||||||
|  | |||||||
| @ -3,15 +3,21 @@ package initializers | |||||||
| import ( | import ( | ||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"github.com/joho/godotenv" | 	"github.com/joho/godotenv" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func LoadEnvVariables() { | func LoadEnvVariables() { | ||||||
| 	err := godotenv.Load() | 	LoadEnvVariablesPath(".") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func LoadEnvVariablesPath(dir string) { | ||||||
|  | 	fp := path.Join(dir, ".env") | ||||||
|  | 	err := godotenv.Load(fp) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal("Error loading .env file") | 		log.Fatal("Error loading .env file", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										34
									
								
								internal/args/lang.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								internal/args/lang.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | package args | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/lang" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type LangVar struct { | ||||||
|  | 	v []lang.Language | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(lv *LangVar) Set(s string) error { | ||||||
|  | 	v, err := lang.LanguageFromCode(s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	lv.v = append(lv.v, v) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(lv *LangVar) String() string { | ||||||
|  | 	var s []string | ||||||
|  | 	for _, v := range(lv.v) { | ||||||
|  | 		s = append(s, v.Code) | ||||||
|  | 	} | ||||||
|  | 	return strings.Join(s, ",") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(lv *LangVar) Langs() []lang.Language { | ||||||
|  | 	return lv.v | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,19 +1,23 @@ | |||||||
| package ussd | package application | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"log" | 	"log" | ||||||
| 	"path" | 	"path" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/cache" | ||||||
| 	"git.defalsify.org/vise.git/lang" | 	"git.defalsify.org/vise.git/lang" | ||||||
| 	"git.defalsify.org/vise.git/persist" | 	"git.defalsify.org/vise.git/persist" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 	"git.defalsify.org/vise.git/state" | 	"git.defalsify.org/vise.git/state" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/mocks" | 	"git.grassecon.net/urdt/ussd/internal/testutil/mocks" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/testservice" | 	"git.grassecon.net/urdt/ussd/internal/testutil/testservice" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/utils" | ||||||
| 	"git.grassecon.net/urdt/ussd/models" | 	"git.grassecon.net/urdt/ussd/models" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/common" | 	"git.grassecon.net/urdt/ussd/common" | ||||||
| @ -22,6 +26,7 @@ import ( | |||||||
| 	testdataloader "github.com/peteole/testdata-loader" | 	testdataloader "github.com/peteole/testdata-loader" | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
| 
 | 
 | ||||||
|  | 	visedb "git.defalsify.org/vise.git/db" | ||||||
| 	memdb "git.defalsify.org/vise.git/db/mem" | 	memdb "git.defalsify.org/vise.git/db/mem" | ||||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||||
| ) | ) | ||||||
| @ -31,6 +36,11 @@ var ( | |||||||
| 	flagsPath = path.Join(baseDir, "services", "registration", "pp.csv") | 	flagsPath = path.Join(baseDir, "services", "registration", "pp.csv") | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // mockReplaceSeparator function
 | ||||||
|  | var mockReplaceSeparator = func(input string) string { | ||||||
|  | 	return strings.ReplaceAll(input, ":", ": ") | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // InitializeTestStore sets up and returns an in-memory database and store.
 | // InitializeTestStore sets up and returns an in-memory database and store.
 | ||||||
| func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) { | func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) { | ||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| @ -50,13 +60,14 @@ func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) | |||||||
| 	return ctx, store | 	return ctx, store | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPrefixDb { | func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *dbstorage.SubPrefixDb { | ||||||
| 	db := memdb.NewMemDb() | 	db := memdb.NewMemDb() | ||||||
| 	err := db.Connect(ctx, "") | 	err := db.Connect(ctx, "") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	spdb := storage.NewSubPrefixDb(db, []byte("vouchers")) | 	prefix := common.ToBytes(visedb.DATATYPE_USERDATA) | ||||||
|  | 	spdb := dbstorage.NewSubPrefixDb(db, prefix) | ||||||
| 
 | 
 | ||||||
| 	return spdb | 	return spdb | ||||||
| } | } | ||||||
| @ -65,12 +76,15 @@ func TestNewHandlers(t *testing.T) { | |||||||
| 	_, store := InitializeTestStore(t) | 	_, store := InitializeTestStore(t) | ||||||
| 
 | 
 | ||||||
| 	fm, err := NewFlagManager(flagsPath) | 	fm, err := NewFlagManager(flagsPath) | ||||||
| 	accountService := testservice.TestAccountService{} |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Logf(err.Error()) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	accountService := testservice.TestAccountService{} | ||||||
|  | 
 | ||||||
|  | 	// Test case for valid UserDataStore
 | ||||||
| 	t.Run("Valid UserDataStore", func(t *testing.T) { | 	t.Run("Valid UserDataStore", func(t *testing.T) { | ||||||
| 		handlers, err := NewHandlers(fm.parser, store, nil, &accountService) | 		handlers, err := NewHandlers(fm.parser, store, nil, &accountService, mockReplaceSeparator) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatalf("expected no error, got %v", err) | 			t.Fatalf("expected no error, got %v", err) | ||||||
| 		} | 		} | ||||||
| @ -80,23 +94,130 @@ func TestNewHandlers(t *testing.T) { | |||||||
| 		if handlers.userdataStore == nil { | 		if handlers.userdataStore == nil { | ||||||
| 			t.Fatal("expected userdataStore to be set in handlers") | 			t.Fatal("expected userdataStore to be set in handlers") | ||||||
| 		} | 		} | ||||||
|  | 		if handlers.ReplaceSeparatorFunc == nil { | ||||||
|  | 			t.Fatal("expected ReplaceSeparatorFunc to be set in handlers") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Test ReplaceSeparatorFunc functionality
 | ||||||
|  | 		input := "1:Menu item" | ||||||
|  | 		expectedOutput := "1: Menu item" | ||||||
|  | 		if handlers.ReplaceSeparatorFunc(input) != expectedOutput { | ||||||
|  | 			t.Fatalf("ReplaceSeparatorFunc function did not return expected output: got %v, want %v", handlers.ReplaceSeparatorFunc(input), expectedOutput) | ||||||
|  | 		} | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	// Test case for nil userdataStore
 | 	// Test case for nil UserDataStore
 | ||||||
| 	t.Run("Nil UserDataStore", func(t *testing.T) { | 	t.Run("Nil UserDataStore", func(t *testing.T) { | ||||||
| 		handlers, err := NewHandlers(fm.parser, nil, nil, &accountService) | 		handlers, err := NewHandlers(fm.parser, nil, nil, &accountService, mockReplaceSeparator) | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
| 			t.Fatal("expected an error, got none") | 			t.Fatal("expected an error, got none") | ||||||
| 		} | 		} | ||||||
| 		if handlers != nil { | 		if handlers != nil { | ||||||
| 			t.Fatal("expected handlers to be nil") | 			t.Fatal("expected handlers to be nil") | ||||||
| 		} | 		} | ||||||
| 		if err.Error() != "cannot create handler with nil userdata store" { | 		expectedError := "cannot create handler with nil userdata store" | ||||||
| 			t.Fatalf("expected specific error, got %v", err) | 		if err.Error() != expectedError { | ||||||
|  | 			t.Fatalf("expected error '%s', got '%v'", expectedError, err) | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestInit(t *testing.T) { | ||||||
|  | 	sessionId := "session123" | ||||||
|  | 	ctx, store := InitializeTestStore(t) | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 
 | ||||||
|  | 	fm, err := NewFlagManager(flagsPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	adminstore, err := utils.NewAdminStore(ctx, "admin_numbers") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	st := state.NewState(128) | ||||||
|  | 	ca := cache.NewCache() | ||||||
|  | 
 | ||||||
|  | 	flag_admin_privilege, _ := fm.GetFlag("flag_admin_privilege") | ||||||
|  | 
 | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name           string | ||||||
|  | 		setup          func() (*Handlers, context.Context) | ||||||
|  | 		input          []byte | ||||||
|  | 		expectedResult resource.Result | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "Handler not ready", | ||||||
|  | 			setup: func() (*Handlers, context.Context) { | ||||||
|  | 				return &Handlers{}, ctx | ||||||
|  | 			}, | ||||||
|  | 			input:          []byte("1"), | ||||||
|  | 			expectedResult: resource.Result{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "State and memory initialization", | ||||||
|  | 			setup: func() (*Handlers, context.Context) { | ||||||
|  | 				pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca) | ||||||
|  | 				h := &Handlers{ | ||||||
|  | 					flagManager: fm.parser, | ||||||
|  | 					adminstore:  adminstore, | ||||||
|  | 					pe:          pe, | ||||||
|  | 				} | ||||||
|  | 				return h, context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 			}, | ||||||
|  | 			input: []byte("1"), | ||||||
|  | 			expectedResult: resource.Result{ | ||||||
|  | 				FlagReset: []uint32{flag_admin_privilege}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Non-admin session initialization", | ||||||
|  | 			setup: func() (*Handlers, context.Context) { | ||||||
|  | 				pe := persist.NewPersister(store).WithSession("0712345678").WithContent(st, ca) | ||||||
|  | 				h := &Handlers{ | ||||||
|  | 					flagManager: fm.parser, | ||||||
|  | 					adminstore:  adminstore, | ||||||
|  | 					pe:          pe, | ||||||
|  | 				} | ||||||
|  | 				return h, context.WithValue(context.Background(), "SessionId", "0712345678") | ||||||
|  | 			}, | ||||||
|  | 			input: []byte("1"), | ||||||
|  | 			expectedResult: resource.Result{ | ||||||
|  | 				FlagReset: []uint32{flag_admin_privilege}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Move to top node on empty input", | ||||||
|  | 			setup: func() (*Handlers, context.Context) { | ||||||
|  | 				pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca) | ||||||
|  | 				h := &Handlers{ | ||||||
|  | 					flagManager: fm.parser, | ||||||
|  | 					adminstore:  adminstore, | ||||||
|  | 					pe:          pe, | ||||||
|  | 				} | ||||||
|  | 				st.Code = []byte("some pending bytecode") | ||||||
|  | 				return h, context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 			}, | ||||||
|  | 			input: []byte(""), | ||||||
|  | 			expectedResult: resource.Result{ | ||||||
|  | 				FlagReset: []uint32{flag_admin_privilege}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			h, testCtx := tt.setup() | ||||||
|  | 			res, err := h.Init(testCtx, "", tt.input) | ||||||
|  | 
 | ||||||
|  | 			assert.NoError(t, err, "Unexpected error occurred") | ||||||
|  | 			assert.Equal(t, res, tt.expectedResult, "Expected result should match actual result") | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestCreateAccount(t *testing.T) { | func TestCreateAccount(t *testing.T) { | ||||||
| 	sessionId := "session123" | 	sessionId := "session123" | ||||||
| 	ctx, store := InitializeTestStore(t) | 	ctx, store := InitializeTestStore(t) | ||||||
| @ -179,11 +300,14 @@ func TestSaveFirstname(t *testing.T) { | |||||||
| 	fm, _ := NewFlagManager(flagsPath) | 	fm, _ := NewFlagManager(flagsPath) | ||||||
| 
 | 
 | ||||||
| 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | ||||||
|  | 	flag_firstname_set, _ := fm.GetFlag("flag_firstname_set") | ||||||
| 
 | 
 | ||||||
| 	// Set the flag in the State
 | 	// Set the flag in the State
 | ||||||
| 	mockState := state.NewState(16) | 	mockState := state.NewState(128) | ||||||
| 	mockState.SetFlag(flag_allow_update) | 	mockState.SetFlag(flag_allow_update) | ||||||
| 
 | 
 | ||||||
|  | 	expectedResult := resource.Result{} | ||||||
|  | 
 | ||||||
| 	// Define test data
 | 	// Define test data
 | ||||||
| 	firstName := "John" | 	firstName := "John" | ||||||
| 
 | 
 | ||||||
| @ -191,6 +315,8 @@ func TestSaveFirstname(t *testing.T) { | |||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	expectedResult.FlagSet = []uint32{flag_firstname_set} | ||||||
|  | 
 | ||||||
| 	// Create the Handlers instance with the mock store
 | 	// Create the Handlers instance with the mock store
 | ||||||
| 	h := &Handlers{ | 	h := &Handlers{ | ||||||
| 		userdataStore: store, | 		userdataStore: store, | ||||||
| @ -203,7 +329,7 @@ func TestSaveFirstname(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	// Assert results
 | 	// Assert results
 | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, resource.Result{}, res) | 	assert.Equal(t, expectedResult, res) | ||||||
| 
 | 
 | ||||||
| 	// Verify that the DATA_FIRST_NAME entry has been updated with the temporary value
 | 	// Verify that the DATA_FIRST_NAME entry has been updated with the temporary value
 | ||||||
| 	storedFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME) | 	storedFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME) | ||||||
| @ -218,11 +344,16 @@ func TestSaveFamilyname(t *testing.T) { | |||||||
| 	fm, _ := NewFlagManager(flagsPath) | 	fm, _ := NewFlagManager(flagsPath) | ||||||
| 
 | 
 | ||||||
| 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | ||||||
|  | 	flag_firstname_set, _ := fm.GetFlag("flag_familyname_set") | ||||||
| 
 | 
 | ||||||
| 	// Set the flag in the State
 | 	// Set the flag in the State
 | ||||||
| 	mockState := state.NewState(16) | 	mockState := state.NewState(128) | ||||||
| 	mockState.SetFlag(flag_allow_update) | 	mockState.SetFlag(flag_allow_update) | ||||||
| 
 | 
 | ||||||
|  | 	expectedResult := resource.Result{} | ||||||
|  | 
 | ||||||
|  | 	expectedResult.FlagSet = []uint32{flag_firstname_set} | ||||||
|  | 
 | ||||||
| 	// Define test data
 | 	// Define test data
 | ||||||
| 	familyName := "Doeee" | 	familyName := "Doeee" | ||||||
| 
 | 
 | ||||||
| @ -242,7 +373,7 @@ func TestSaveFamilyname(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	// Assert results
 | 	// Assert results
 | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, resource.Result{}, res) | 	assert.Equal(t, expectedResult, res) | ||||||
| 
 | 
 | ||||||
| 	// Verify that the DATA_FAMILY_NAME entry has been updated with the temporary value
 | 	// Verify that the DATA_FAMILY_NAME entry has been updated with the temporary value
 | ||||||
| 	storedFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME) | 	storedFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME) | ||||||
| @ -257,11 +388,14 @@ func TestSaveYoB(t *testing.T) { | |||||||
| 	fm, _ := NewFlagManager(flagsPath) | 	fm, _ := NewFlagManager(flagsPath) | ||||||
| 
 | 
 | ||||||
| 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | ||||||
|  | 	flag_yob_set, _ := fm.GetFlag("flag_yob_set") | ||||||
| 
 | 
 | ||||||
| 	// Set the flag in the State
 | 	// Set the flag in the State
 | ||||||
| 	mockState := state.NewState(16) | 	mockState := state.NewState(108) | ||||||
| 	mockState.SetFlag(flag_allow_update) | 	mockState.SetFlag(flag_allow_update) | ||||||
| 
 | 
 | ||||||
|  | 	expectedResult := resource.Result{} | ||||||
|  | 
 | ||||||
| 	// Define test data
 | 	// Define test data
 | ||||||
| 	yob := "1980" | 	yob := "1980" | ||||||
| 
 | 
 | ||||||
| @ -269,6 +403,8 @@ func TestSaveYoB(t *testing.T) { | |||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	expectedResult.FlagSet = []uint32{flag_yob_set} | ||||||
|  | 
 | ||||||
| 	// Create the Handlers instance with the mock store
 | 	// Create the Handlers instance with the mock store
 | ||||||
| 	h := &Handlers{ | 	h := &Handlers{ | ||||||
| 		userdataStore: store, | 		userdataStore: store, | ||||||
| @ -281,7 +417,7 @@ func TestSaveYoB(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	// Assert results
 | 	// Assert results
 | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, resource.Result{}, res) | 	assert.Equal(t, expectedResult, res) | ||||||
| 
 | 
 | ||||||
| 	// Verify that the DATA_YOB entry has been updated with the temporary value
 | 	// Verify that the DATA_YOB entry has been updated with the temporary value
 | ||||||
| 	storedYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_YOB) | 	storedYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_YOB) | ||||||
| @ -296,11 +432,14 @@ func TestSaveLocation(t *testing.T) { | |||||||
| 	fm, _ := NewFlagManager(flagsPath) | 	fm, _ := NewFlagManager(flagsPath) | ||||||
| 
 | 
 | ||||||
| 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | ||||||
|  | 	flag_location_set, _ := fm.GetFlag("flag_location_set") | ||||||
| 
 | 
 | ||||||
| 	// Set the flag in the State
 | 	// Set the flag in the State
 | ||||||
| 	mockState := state.NewState(16) | 	mockState := state.NewState(108) | ||||||
| 	mockState.SetFlag(flag_allow_update) | 	mockState.SetFlag(flag_allow_update) | ||||||
| 
 | 
 | ||||||
|  | 	expectedResult := resource.Result{} | ||||||
|  | 
 | ||||||
| 	// Define test data
 | 	// Define test data
 | ||||||
| 	location := "Kilifi" | 	location := "Kilifi" | ||||||
| 
 | 
 | ||||||
| @ -308,6 +447,8 @@ func TestSaveLocation(t *testing.T) { | |||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	expectedResult.FlagSet = []uint32{flag_location_set} | ||||||
|  | 
 | ||||||
| 	// Create the Handlers instance with the mock store
 | 	// Create the Handlers instance with the mock store
 | ||||||
| 	h := &Handlers{ | 	h := &Handlers{ | ||||||
| 		userdataStore: store, | 		userdataStore: store, | ||||||
| @ -320,7 +461,7 @@ func TestSaveLocation(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	// Assert results
 | 	// Assert results
 | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, resource.Result{}, res) | 	assert.Equal(t, expectedResult, res) | ||||||
| 
 | 
 | ||||||
| 	// Verify that the DATA_LOCATION entry has been updated with the temporary value
 | 	// Verify that the DATA_LOCATION entry has been updated with the temporary value
 | ||||||
| 	storedLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_LOCATION) | 	storedLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_LOCATION) | ||||||
| @ -335,11 +476,14 @@ func TestSaveOfferings(t *testing.T) { | |||||||
| 	fm, _ := NewFlagManager(flagsPath) | 	fm, _ := NewFlagManager(flagsPath) | ||||||
| 
 | 
 | ||||||
| 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | ||||||
|  | 	flag_offerings_set, _ := fm.GetFlag("flag_offerings_set") | ||||||
| 
 | 
 | ||||||
| 	// Set the flag in the State
 | 	// Set the flag in the State
 | ||||||
| 	mockState := state.NewState(16) | 	mockState := state.NewState(108) | ||||||
| 	mockState.SetFlag(flag_allow_update) | 	mockState.SetFlag(flag_allow_update) | ||||||
| 
 | 
 | ||||||
|  | 	expectedResult := resource.Result{} | ||||||
|  | 
 | ||||||
| 	// Define test data
 | 	// Define test data
 | ||||||
| 	offerings := "Bananas" | 	offerings := "Bananas" | ||||||
| 
 | 
 | ||||||
| @ -347,6 +491,8 @@ func TestSaveOfferings(t *testing.T) { | |||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	expectedResult.FlagSet = []uint32{flag_offerings_set} | ||||||
|  | 
 | ||||||
| 	// Create the Handlers instance with the mock store
 | 	// Create the Handlers instance with the mock store
 | ||||||
| 	h := &Handlers{ | 	h := &Handlers{ | ||||||
| 		userdataStore: store, | 		userdataStore: store, | ||||||
| @ -359,7 +505,7 @@ func TestSaveOfferings(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	// Assert results
 | 	// Assert results
 | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, resource.Result{}, res) | 	assert.Equal(t, expectedResult, res) | ||||||
| 
 | 
 | ||||||
| 	// Verify that the DATA_OFFERINGS entry has been updated with the temporary value
 | 	// Verify that the DATA_OFFERINGS entry has been updated with the temporary value
 | ||||||
| 	storedOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS) | 	storedOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS) | ||||||
| @ -374,9 +520,10 @@ func TestSaveGender(t *testing.T) { | |||||||
| 	fm, _ := NewFlagManager(flagsPath) | 	fm, _ := NewFlagManager(flagsPath) | ||||||
| 
 | 
 | ||||||
| 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | 	flag_allow_update, _ := fm.GetFlag("flag_allow_update") | ||||||
|  | 	flag_gender_set, _ := fm.GetFlag("flag_gender_set") | ||||||
| 
 | 
 | ||||||
| 	// Set the flag in the State
 | 	// Set the flag in the State
 | ||||||
| 	mockState := state.NewState(16) | 	mockState := state.NewState(108) | ||||||
| 	mockState.SetFlag(flag_allow_update) | 	mockState.SetFlag(flag_allow_update) | ||||||
| 
 | 
 | ||||||
| 	// Define test cases
 | 	// Define test cases
 | ||||||
| @ -420,12 +567,16 @@ func TestSaveGender(t *testing.T) { | |||||||
| 				flagManager:   fm.parser, | 				flagManager:   fm.parser, | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | 			expectedResult := resource.Result{} | ||||||
|  | 
 | ||||||
| 			// Call the method
 | 			// Call the method
 | ||||||
| 			res, err := h.SaveGender(ctx, "save_gender", tt.input) | 			res, err := h.SaveGender(ctx, "save_gender", tt.input) | ||||||
| 
 | 
 | ||||||
|  | 			expectedResult.FlagSet = []uint32{flag_gender_set} | ||||||
|  | 
 | ||||||
| 			// Assert results
 | 			// Assert results
 | ||||||
| 			assert.NoError(t, err) | 			assert.NoError(t, err) | ||||||
| 			assert.Equal(t, resource.Result{}, res) | 			assert.Equal(t, expectedResult, res) | ||||||
| 
 | 
 | ||||||
| 			// Verify that the DATA_GENDER entry has been updated with the temporary value
 | 			// Verify that the DATA_GENDER entry has been updated with the temporary value
 | ||||||
| 			storedGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_GENDER) | 			storedGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_GENDER) | ||||||
| @ -624,6 +775,11 @@ func TestSetLanguage(t *testing.T) { | |||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	sessionId := "session123" | ||||||
|  | 	ctx, store := InitializeTestStore(t) | ||||||
|  | 
 | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 
 | ||||||
| 	// Define test cases
 | 	// Define test cases
 | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		name           string | 		name           string | ||||||
| @ -657,11 +813,12 @@ func TestSetLanguage(t *testing.T) { | |||||||
| 			// Create the Handlers instance with the mock flag manager
 | 			// Create the Handlers instance with the mock flag manager
 | ||||||
| 			h := &Handlers{ | 			h := &Handlers{ | ||||||
| 				flagManager:   fm.parser, | 				flagManager:   fm.parser, | ||||||
|  | 				userdataStore: store, | ||||||
| 				st:            mockState, | 				st:            mockState, | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// Call the method
 | 			// Call the method
 | ||||||
| 			res, err := h.SetLanguage(context.Background(), "set_language", nil) | 			res, err := h.SetLanguage(ctx, "set_language", nil) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Error(err) | 				t.Error(err) | ||||||
| 			} | 			} | ||||||
| @ -757,37 +914,79 @@ func TestResetAccountAuthorized(t *testing.T) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestIncorrectPinReset(t *testing.T) { | func TestIncorrectPinReset(t *testing.T) { | ||||||
|  | 	sessionId := "session123" | ||||||
|  | 	ctx, store := InitializeTestStore(t) | ||||||
| 	fm, err := NewFlagManager(flagsPath) | 	fm, err := NewFlagManager(flagsPath) | ||||||
|  | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin") | 	flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin") | ||||||
|  | 	flag_account_blocked, _ := fm.parser.GetFlag("flag_account_blocked") | ||||||
|  | 
 | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
| 
 | 
 | ||||||
| 	// Define test cases
 | 	// Define test cases
 | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		name           string | 		name           string | ||||||
| 		input          []byte | 		input          []byte | ||||||
|  | 		attempts       uint8 | ||||||
| 		expectedResult resource.Result | 		expectedResult resource.Result | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name:  "Test incorrect pin reset", | 			name:  "Test when incorrect PIN attempts is 2", | ||||||
| 			input: []byte(""), | 			input: []byte(""), | ||||||
| 			expectedResult: resource.Result{ | 			expectedResult: resource.Result{ | ||||||
| 				FlagReset: []uint32{flag_incorrect_pin}, | 				FlagReset: []uint32{flag_incorrect_pin}, | ||||||
|  | 				Content:   "1", //Expected remaining PIN attempts
 | ||||||
| 			}, | 			}, | ||||||
|  | 			attempts: 2, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:  "Test incorrect pin reset when incorrect PIN attempts is 1", | ||||||
|  | 			input: []byte(""), | ||||||
|  | 			expectedResult: resource.Result{ | ||||||
|  | 				FlagReset: []uint32{flag_incorrect_pin}, | ||||||
|  | 				Content:   "2", //Expected remaining PIN attempts
 | ||||||
|  | 			}, | ||||||
|  | 			attempts: 1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:  "Test incorrect pin reset when incorrect PIN attempts is 1", | ||||||
|  | 			input: []byte(""), | ||||||
|  | 			expectedResult: resource.Result{ | ||||||
|  | 				FlagReset: []uint32{flag_incorrect_pin}, | ||||||
|  | 				Content:   "2", //Expected remaining PIN attempts
 | ||||||
|  | 			}, | ||||||
|  | 			attempts: 1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:  "Test incorrect pin reset when incorrect PIN attempts is 3(account expected to be blocked)", | ||||||
|  | 			input: []byte(""), | ||||||
|  | 			expectedResult: resource.Result{ | ||||||
|  | 				FlagReset: []uint32{flag_incorrect_pin}, | ||||||
|  | 				FlagSet:   []uint32{flag_account_blocked}, | ||||||
|  | 			}, | ||||||
|  | 			attempts: 3, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 
 | ||||||
|  | 			if err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(tt.attempts)))); err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
| 			// Create the Handlers instance with the mock flag manager
 | 			// Create the Handlers instance with the mock flag manager
 | ||||||
| 			h := &Handlers{ | 			h := &Handlers{ | ||||||
| 				flagManager:   fm.parser, | 				flagManager:   fm.parser, | ||||||
|  | 				userdataStore: store, | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// Call the method
 | 			// Call the method
 | ||||||
| 			res, err := h.ResetIncorrectPin(context.Background(), "reset_incorrect_pin", tt.input) | 			res, err := h.ResetIncorrectPin(ctx, "reset_incorrect_pin", tt.input) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Error(err) | 				t.Error(err) | ||||||
| 			} | 			} | ||||||
| @ -897,7 +1096,14 @@ func TestAuthorize(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		t.Run(tt.name, func(t *testing.T) { | ||||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(accountPIN)) | 			// Hash the PIN
 | ||||||
|  | 			hashedPIN, err := common.HashPIN(accountPIN) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err) | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN)) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Fatal(err) | 				t.Fatal(err) | ||||||
| 			} | 			} | ||||||
| @ -1349,59 +1555,6 @@ func TestQuit(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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 TestValidateAmount(t *testing.T) { | func TestValidateAmount(t *testing.T) { | ||||||
| 	fm, err := NewFlagManager(flagsPath) | 	fm, err := NewFlagManager(flagsPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -1498,10 +1651,10 @@ func TestValidateRecipient(t *testing.T) { | |||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name:  "Test with invalid recepient", | 			name:  "Test with invalid recepient", | ||||||
| 			input: []byte("9234adf5"), | 			input: []byte("7?1234"), | ||||||
| 			expectedResult: resource.Result{ | 			expectedResult: resource.Result{ | ||||||
| 				FlagSet: []uint32{flag_invalid_recipient}, | 				FlagSet: []uint32{flag_invalid_recipient}, | ||||||
| 				Content: "9234adf5", | 				Content: "7?1234", | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| @ -1517,22 +1670,40 @@ func TestValidateRecipient(t *testing.T) { | |||||||
| 			input:          []byte("0711223344"), | 			input:          []byte("0711223344"), | ||||||
| 			expectedResult: resource.Result{}, | 			expectedResult: resource.Result{}, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:           "Test with address", | ||||||
|  | 			input:          []byte("0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9"), | ||||||
|  | 			expectedResult: resource.Result{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:           "Test with alias recepient", | ||||||
|  | 			input:          []byte("alias123"), | ||||||
|  | 			expectedResult: resource.Result{}, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// store a public key for the valid recipient
 | 	// store a public key for the valid recipient
 | ||||||
| 	err = store.WriteEntry(ctx, "0711223344", common.DATA_PUBLIC_KEY, []byte(publicKey)) | 	err = store.WriteEntry(ctx, "+254711223344", common.DATA_PUBLIC_KEY, []byte(publicKey)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			mockAccountService := new(mocks.MockAccountService) | ||||||
| 			// Create the Handlers instance
 | 			// Create the Handlers instance
 | ||||||
| 			h := &Handlers{ | 			h := &Handlers{ | ||||||
| 				flagManager:    fm.parser, | 				flagManager:    fm.parser, | ||||||
| 				userdataStore:  store, | 				userdataStore:  store, | ||||||
|  | 				accountService: mockAccountService, | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | 			aliasResponse := &dataserviceapi.AliasAddress{ | ||||||
|  | 				Address: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			mockAccountService.On("CheckAliasAddress", string(tt.input)).Return(aliasResponse, nil) | ||||||
|  | 
 | ||||||
| 			// Call the method
 | 			// Call the method
 | ||||||
| 			res, err := h.ValidateRecipient(ctx, "validate_recepient", tt.input) | 			res, err := h.ValidateRecipient(ctx, "validate_recepient", tt.input) | ||||||
| 
 | 
 | ||||||
| @ -1564,7 +1735,7 @@ func TestCheckBalance(t *testing.T) { | |||||||
| 			publicKey:      "0X98765432109", | 			publicKey:      "0X98765432109", | ||||||
| 			activeSym:      "ETH", | 			activeSym:      "ETH", | ||||||
| 			activeBal:      "1.5", | 			activeBal:      "1.5", | ||||||
| 			expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"}, | 			expectedResult: resource.Result{Content: "Balance: 1.50 ETH\n"}, | ||||||
| 			expectError:    false, | 			expectError:    false, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| @ -1630,7 +1801,7 @@ func TestGetProfile(t *testing.T) { | |||||||
| 			result: resource.Result{ | 			result: resource.Result{ | ||||||
| 				Content: fmt.Sprintf( | 				Content: fmt.Sprintf( | ||||||
| 					"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", | 					"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", | ||||||
| 					"John Doee", "Male", "48", "Kilifi", "Bananas", | 					"John Doee", "Male", "49", "Kilifi", "Bananas", | ||||||
| 				), | 				), | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| @ -1642,7 +1813,7 @@ func TestGetProfile(t *testing.T) { | |||||||
| 			result: resource.Result{ | 			result: resource.Result{ | ||||||
| 				Content: fmt.Sprintf( | 				Content: fmt.Sprintf( | ||||||
| 					"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n", | 					"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n", | ||||||
| 					"John Doee", "Male", "48", "Kilifi", "Bananas", | 					"John Doee", "Male", "49", "Kilifi", "Bananas", | ||||||
| 				), | 				), | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| @ -1654,7 +1825,7 @@ func TestGetProfile(t *testing.T) { | |||||||
| 			result: resource.Result{ | 			result: resource.Result{ | ||||||
| 				Content: fmt.Sprintf( | 				Content: fmt.Sprintf( | ||||||
| 					"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", | 					"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n", | ||||||
| 					"John Doee", "Male", "48", "Kilifi", "Bananas", | 					"John Doee", "Male", "49", "Kilifi", "Bananas", | ||||||
| 				), | 				), | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| @ -1919,7 +2090,7 @@ func TestCheckVouchers(t *testing.T) { | |||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 
 | 
 | ||||||
| 	// Read voucher sym data from the store
 | 	// Read voucher sym data from the store
 | ||||||
| 	voucherData, err := spdb.Get(ctx, []byte("sym")) | 	voucherData, err := spdb.Get(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @ -1932,26 +2103,31 @@ func TestCheckVouchers(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| func TestGetVoucherList(t *testing.T) { | func TestGetVoucherList(t *testing.T) { | ||||||
| 	sessionId := "session123" | 	sessionId := "session123" | ||||||
|  | 
 | ||||||
| 	ctx := context.WithValue(context.Background(), "SessionId", sessionId) | 	ctx := context.WithValue(context.Background(), "SessionId", sessionId) | ||||||
| 
 | 
 | ||||||
| 	spdb := InitializeTestSubPrefixDb(t, ctx) | 	spdb := InitializeTestSubPrefixDb(t, ctx) | ||||||
| 
 | 
 | ||||||
|  | 	// Initialize Handlers
 | ||||||
| 	h := &Handlers{ | 	h := &Handlers{ | ||||||
| 		prefixDb:             spdb, | 		prefixDb:             spdb, | ||||||
|  | 		ReplaceSeparatorFunc: mockReplaceSeparator, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	expectedSym := []byte("1:SRF\n2:MILO") | 	mockSyms := []byte("1:SRF\n2:MILO") | ||||||
| 
 | 
 | ||||||
| 	// Put voucher sym data from the store
 | 	// Put voucher sym data from the store
 | ||||||
| 	err := spdb.Put(ctx, []byte("sym"), expectedSym) | 	err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), mockSyms) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	expectedSyms := []byte("1: SRF\n2: MILO") | ||||||
|  | 
 | ||||||
| 	res, err := h.GetVoucherList(ctx, "", []byte("")) | 	res, err := h.GetVoucherList(ctx, "", []byte("")) | ||||||
| 
 | 
 | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, res.Content, string(expectedSym)) | 	assert.Equal(t, res.Content, string(expectedSyms)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestViewVoucher(t *testing.T) { | func TestViewVoucher(t *testing.T) { | ||||||
| @ -1973,16 +2149,16 @@ func TestViewVoucher(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Define mock voucher data
 | 	// Define mock voucher data
 | ||||||
| 	mockData := map[string][]byte{ | 	mockData := map[common.DataTyp][]byte{ | ||||||
| 		"sym":  []byte("1:SRF\n2:MILO"), | 		common.DATA_VOUCHER_SYMBOLS:   []byte("1:SRF\n2:MILO"), | ||||||
| 		"bal":  []byte("1:100\n2:200"), | 		common.DATA_VOUCHER_BALANCES:  []byte("1:100\n2:200"), | ||||||
| 		"deci": []byte("1:6\n2:4"), | 		common.DATA_VOUCHER_DECIMALS:  []byte("1:6\n2:4"), | ||||||
| 		"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), | 		common.DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Put the data
 | 	// Put the data
 | ||||||
| 	for key, value := range mockData { | 	for key, value := range mockData { | ||||||
| 		err = spdb.Put(ctx, []byte(key), []byte(value)) | 		err = spdb.Put(ctx, []byte(common.ToBytes(key)), []byte(value)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatal(err) | 			t.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -1990,7 +2166,7 @@ func TestViewVoucher(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1")) | 	res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1")) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, res.Content, "SRF\n100") | 	assert.Equal(t, res.Content, "Symbol: SRF\nBalance: 100") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestSetVoucher(t *testing.T) { | func TestSetVoucher(t *testing.T) { | ||||||
| @ -2024,3 +2200,132 @@ func TestSetVoucher(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 	assert.Equal(t, string(tempData.TokenSymbol), res.Content) | 	assert.Equal(t, string(tempData.TokenSymbol), res.Content) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestGetVoucherDetails(t *testing.T) { | ||||||
|  | 	ctx, store := InitializeTestStore(t) | ||||||
|  | 	fm, err := NewFlagManager(flagsPath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Logf(err.Error()) | ||||||
|  | 	} | ||||||
|  | 	mockAccountService := new(mocks.MockAccountService) | ||||||
|  | 
 | ||||||
|  | 	sessionId := "session123" | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 	expectedResult := resource.Result{} | ||||||
|  | 
 | ||||||
|  | 	tokA_AAddress := "0x0000000000000000000000000000000000000000" | ||||||
|  | 
 | ||||||
|  | 	h := &Handlers{ | ||||||
|  | 		userdataStore:  store, | ||||||
|  | 		flagManager:    fm.parser, | ||||||
|  | 		accountService: mockAccountService, | ||||||
|  | 	} | ||||||
|  | 	err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_ADDRESS, []byte(tokA_AAddress)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	tokenDetails := &models.VoucherDataResult{ | ||||||
|  | 		TokenName:      "Token A", | ||||||
|  | 		TokenSymbol:    "TOKA", | ||||||
|  | 		TokenLocation:  "Kilifi,Kenya", | ||||||
|  | 		TokenCommodity: "Farming", | ||||||
|  | 	} | ||||||
|  | 	expectedResult.Content = fmt.Sprintf( | ||||||
|  | 		"Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", tokenDetails.TokenName, tokenDetails.TokenSymbol, tokenDetails.TokenCommodity, tokenDetails.TokenLocation, | ||||||
|  | 	) | ||||||
|  | 	mockAccountService.On("VoucherData", string(tokA_AAddress)).Return(tokenDetails, nil) | ||||||
|  | 
 | ||||||
|  | 	res, err := h.GetVoucherDetails(ctx, "SessionId", []byte("")) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, expectedResult, res) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestCountIncorrectPINAttempts(t *testing.T) { | ||||||
|  | 	ctx, store := InitializeTestStore(t) | ||||||
|  | 	sessionId := "session123" | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 	attempts := uint8(2) | ||||||
|  | 
 | ||||||
|  | 	h := &Handlers{ | ||||||
|  | 		userdataStore: store, | ||||||
|  | 	} | ||||||
|  | 	err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(attempts)))) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Logf(err.Error()) | ||||||
|  | 	} | ||||||
|  | 	err = h.incrementIncorrectPINAttempts(ctx, sessionId) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Logf(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	attemptsAfterCount, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Logf(err.Error()) | ||||||
|  | 	} | ||||||
|  | 	pinAttemptsValue, _ := strconv.ParseUint(string(attemptsAfterCount), 0, 64) | ||||||
|  | 	pinAttemptsCount := uint8(pinAttemptsValue) | ||||||
|  | 	expectedAttempts := attempts + 1 | ||||||
|  | 	assert.Equal(t, pinAttemptsCount, expectedAttempts) | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestResetIncorrectPINAttempts(t *testing.T) { | ||||||
|  | 	ctx, store := InitializeTestStore(t) | ||||||
|  | 	sessionId := "session123" | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 
 | ||||||
|  | 	err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(string("2"))) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Logf(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	h := &Handlers{ | ||||||
|  | 		userdataStore: store, | ||||||
|  | 	} | ||||||
|  | 	h.resetIncorrectPINAttempts(ctx, sessionId) | ||||||
|  | 	incorrectAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Logf(err.Error()) | ||||||
|  | 	} | ||||||
|  | 	assert.Equal(t, "0", string(incorrectAttempts)) | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestPersistLanguageCode(t *testing.T) { | ||||||
|  | 	ctx, store := InitializeTestStore(t) | ||||||
|  | 
 | ||||||
|  | 	sessionId := "session123" | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 
 | ||||||
|  | 	h := &Handlers{ | ||||||
|  | 		userdataStore: store, | ||||||
|  | 	} | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name                 string | ||||||
|  | 		code                 string | ||||||
|  | 		expectedLanguageCode string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:                 "Set Default Language (English)", | ||||||
|  | 			code:                 "eng", | ||||||
|  | 			expectedLanguageCode: "eng", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:                 "Set Swahili Language", | ||||||
|  | 			code:                 "swa", | ||||||
|  | 			expectedLanguageCode: "swa", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, test := range tests { | ||||||
|  | 		err := h.persistLanguageCode(ctx, test.code) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Logf(err.Error()) | ||||||
|  | 		} | ||||||
|  | 		code, err := store.ReadEntry(ctx, sessionId, common.DATA_SELECTED_LANGUAGE_CODE) | ||||||
|  | 
 | ||||||
|  | 		assert.Equal(t, test.expectedLanguageCode, string(code)) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -6,7 +6,7 @@ import ( | |||||||
| 	"git.defalsify.org/vise.git/persist" | 	"git.defalsify.org/vise.git/persist" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/ussd" | 	"git.grassecon.net/urdt/ussd/internal/handlers/application" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -14,11 +14,11 @@ type BaseSessionHandler struct { | |||||||
| 	cfgTemplate engine.Config | 	cfgTemplate engine.Config | ||||||
| 	rp          RequestParser | 	rp          RequestParser | ||||||
| 	rs          resource.Resource | 	rs          resource.Resource | ||||||
| 	hn *ussd.Handlers | 	hn          *application.Handlers | ||||||
| 	provider    storage.StorageProvider | 	provider    storage.StorageProvider | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler { | func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *application.Handlers) *BaseSessionHandler { | ||||||
| 	return &BaseSessionHandler{ | 	return &BaseSessionHandler{ | ||||||
| 		cfgTemplate: cfg, | 		cfgTemplate: cfg, | ||||||
| 		rs:          rs, | 		rs:          rs, | ||||||
| @ -28,20 +28,20 @@ func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.D | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(f* BaseSessionHandler) Shutdown() { | func (f *BaseSessionHandler) Shutdown() { | ||||||
| 	err := f.provider.Close() | 	err := f.provider.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.Errorf("handler shutdown error", "err", err) | 		logg.Errorf("handler shutdown error", "err", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine { | func (f *BaseSessionHandler) GetEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) engine.Engine { | ||||||
| 	en := engine.NewEngine(cfg, rs) | 	en := engine.NewEngine(cfg, rs) | ||||||
| 	en = en.WithPersister(pr) | 	en = en.WithPersister(pr) | ||||||
| 	return en | 	return en | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) { | func (f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) { | ||||||
| 	var r bool | 	var r bool | ||||||
| 	var err error | 	var err error | ||||||
| 	var ok bool | 	var ok bool | ||||||
| @ -55,6 +55,9 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	f.hn = f.hn.WithPersister(rqs.Storage.Persister) | 	f.hn = f.hn.WithPersister(rqs.Storage.Persister) | ||||||
|  | 	defer func() { | ||||||
|  | 		f.hn.Exit() | ||||||
|  | 	}() | ||||||
| 	eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister) | 	eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister) | ||||||
| 	en, ok := eni.(*engine.DefaultEngine) | 	en, ok := eni.(*engine.DefaultEngine) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| @ -85,21 +88,21 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error) | |||||||
| 	return rqs, nil | 	return rqs, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession,  error) { | func (f *BaseSessionHandler) Output(rqs RequestSession) (RequestSession, error) { | ||||||
| 	var err error | 	var err error | ||||||
| 	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) | 	_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer) | ||||||
| 	return rqs, err | 	return rqs, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) { | func (f *BaseSessionHandler) Reset(rqs RequestSession) (RequestSession, error) { | ||||||
| 	defer f.provider.Put(rqs.Config.SessionId, rqs.Storage) | 	defer f.provider.Put(rqs.Config.SessionId, rqs.Storage) | ||||||
| 	return rqs, rqs.Engine.Finish() | 	return rqs, rqs.Engine.Finish() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(f *BaseSessionHandler) GetConfig() engine.Config { | func (f *BaseSessionHandler) GetConfig() engine.Config { | ||||||
| 	return f.cfgTemplate | 	return f.cfgTemplate | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(f *BaseSessionHandler) GetRequestParser() RequestParser { | func (f *BaseSessionHandler) GetRequestParser() RequestParser { | ||||||
| 	return f.rp | 	return f.rp | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										141
									
								
								internal/handlers/handler_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								internal/handlers/handler_service.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,141 @@ | |||||||
|  | package handlers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/asm" | ||||||
|  | 	"git.defalsify.org/vise.git/db" | ||||||
|  | 	"git.defalsify.org/vise.git/engine" | ||||||
|  | 	"git.defalsify.org/vise.git/persist" | ||||||
|  | 	"git.defalsify.org/vise.git/resource" | ||||||
|  | 
 | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/handlers/application" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/utils" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/remote" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type HandlerService interface { | ||||||
|  | 	GetHandler() (*application.Handlers, error) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getParser(fp string, debug bool) (*asm.FlagParser, error) { | ||||||
|  | 	flagParser := asm.NewFlagParser().WithDebug() | ||||||
|  | 	_, err := flagParser.Load(fp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return flagParser, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type LocalHandlerService struct { | ||||||
|  | 	Parser        *asm.FlagParser | ||||||
|  | 	DbRs          *resource.DbResource | ||||||
|  | 	Pe            *persist.Persister | ||||||
|  | 	UserdataStore *db.Db | ||||||
|  | 	AdminStore    *utils.AdminStore | ||||||
|  | 	Cfg           engine.Config | ||||||
|  | 	Rs            resource.Resource | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) { | ||||||
|  | 	parser, err := getParser(fp, debug) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	adminstore, err := utils.NewAdminStore(ctx, "admin_numbers") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return &LocalHandlerService{ | ||||||
|  | 		Parser:     parser, | ||||||
|  | 		DbRs:       dbResource, | ||||||
|  | 		AdminStore: adminstore, | ||||||
|  | 		Cfg:        cfg, | ||||||
|  | 		Rs:         rs, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (ls *LocalHandlerService) SetPersister(Pe *persist.Persister) { | ||||||
|  | 	ls.Pe = Pe | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (ls *LocalHandlerService) SetDataStore(db *db.Db) { | ||||||
|  | 	ls.UserdataStore = db | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*application.Handlers, error) { | ||||||
|  | 	replaceSeparatorFunc := func(input string) string { | ||||||
|  | 		return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	appHandlers, err := application.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	appHandlers = appHandlers.WithPersister(ls.Pe) | ||||||
|  | 	ls.DbRs.AddLocalFunc("set_language", appHandlers.SetLanguage) | ||||||
|  | 	ls.DbRs.AddLocalFunc("create_account", appHandlers.CreateAccount) | ||||||
|  | 	ls.DbRs.AddLocalFunc("save_temporary_pin", appHandlers.SaveTemporaryPin) | ||||||
|  | 	ls.DbRs.AddLocalFunc("verify_create_pin", appHandlers.VerifyCreatePin) | ||||||
|  | 	ls.DbRs.AddLocalFunc("check_identifier", appHandlers.CheckIdentifier) | ||||||
|  | 	ls.DbRs.AddLocalFunc("check_account_status", appHandlers.CheckAccountStatus) | ||||||
|  | 	ls.DbRs.AddLocalFunc("authorize_account", appHandlers.Authorize) | ||||||
|  | 	ls.DbRs.AddLocalFunc("quit", appHandlers.Quit) | ||||||
|  | 	ls.DbRs.AddLocalFunc("check_balance", appHandlers.CheckBalance) | ||||||
|  | 	ls.DbRs.AddLocalFunc("validate_recipient", appHandlers.ValidateRecipient) | ||||||
|  | 	ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset) | ||||||
|  | 	ls.DbRs.AddLocalFunc("invite_valid_recipient", appHandlers.InviteValidRecipient) | ||||||
|  | 	ls.DbRs.AddLocalFunc("max_amount", appHandlers.MaxAmount) | ||||||
|  | 	ls.DbRs.AddLocalFunc("validate_amount", appHandlers.ValidateAmount) | ||||||
|  | 	ls.DbRs.AddLocalFunc("reset_transaction_amount", appHandlers.ResetTransactionAmount) | ||||||
|  | 	ls.DbRs.AddLocalFunc("get_recipient", appHandlers.GetRecipient) | ||||||
|  | 	ls.DbRs.AddLocalFunc("get_sender", appHandlers.GetSender) | ||||||
|  | 	ls.DbRs.AddLocalFunc("get_amount", appHandlers.GetAmount) | ||||||
|  | 	ls.DbRs.AddLocalFunc("reset_incorrect", appHandlers.ResetIncorrectPin) | ||||||
|  | 	ls.DbRs.AddLocalFunc("save_firstname", appHandlers.SaveFirstname) | ||||||
|  | 	ls.DbRs.AddLocalFunc("save_familyname", appHandlers.SaveFamilyname) | ||||||
|  | 	ls.DbRs.AddLocalFunc("save_gender", appHandlers.SaveGender) | ||||||
|  | 	ls.DbRs.AddLocalFunc("save_location", appHandlers.SaveLocation) | ||||||
|  | 	ls.DbRs.AddLocalFunc("save_yob", appHandlers.SaveYob) | ||||||
|  | 	ls.DbRs.AddLocalFunc("save_offerings", appHandlers.SaveOfferings) | ||||||
|  | 	ls.DbRs.AddLocalFunc("reset_account_authorized", appHandlers.ResetAccountAuthorized) | ||||||
|  | 	ls.DbRs.AddLocalFunc("reset_allow_update", appHandlers.ResetAllowUpdate) | ||||||
|  | 	ls.DbRs.AddLocalFunc("get_profile_info", appHandlers.GetProfileInfo) | ||||||
|  | 	ls.DbRs.AddLocalFunc("verify_yob", appHandlers.VerifyYob) | ||||||
|  | 	ls.DbRs.AddLocalFunc("reset_incorrect_date_format", appHandlers.ResetIncorrectYob) | ||||||
|  | 	ls.DbRs.AddLocalFunc("initiate_transaction", appHandlers.InitiateTransaction) | ||||||
|  | 	ls.DbRs.AddLocalFunc("verify_new_pin", appHandlers.VerifyNewPin) | ||||||
|  | 	ls.DbRs.AddLocalFunc("confirm_pin_change", appHandlers.ConfirmPinChange) | ||||||
|  | 	ls.DbRs.AddLocalFunc("quit_with_help", appHandlers.QuitWithHelp) | ||||||
|  | 	ls.DbRs.AddLocalFunc("fetch_community_balance", appHandlers.FetchCommunityBalance) | ||||||
|  | 	ls.DbRs.AddLocalFunc("set_default_voucher", appHandlers.SetDefaultVoucher) | ||||||
|  | 	ls.DbRs.AddLocalFunc("check_vouchers", appHandlers.CheckVouchers) | ||||||
|  | 	ls.DbRs.AddLocalFunc("get_vouchers", appHandlers.GetVoucherList) | ||||||
|  | 	ls.DbRs.AddLocalFunc("view_voucher", appHandlers.ViewVoucher) | ||||||
|  | 	ls.DbRs.AddLocalFunc("set_voucher", appHandlers.SetVoucher) | ||||||
|  | 	ls.DbRs.AddLocalFunc("get_voucher_details", appHandlers.GetVoucherDetails) | ||||||
|  | 	ls.DbRs.AddLocalFunc("reset_valid_pin", appHandlers.ResetValidPin) | ||||||
|  | 	ls.DbRs.AddLocalFunc("check_pin_mismatch", appHandlers.CheckBlockedNumPinMisMatch) | ||||||
|  | 	ls.DbRs.AddLocalFunc("validate_blocked_number", appHandlers.ValidateBlockedNumber) | ||||||
|  | 	ls.DbRs.AddLocalFunc("retrieve_blocked_number", appHandlers.RetrieveBlockedNumber) | ||||||
|  | 	ls.DbRs.AddLocalFunc("reset_unregistered_number", appHandlers.ResetUnregisteredNumber) | ||||||
|  | 	ls.DbRs.AddLocalFunc("reset_others_pin", appHandlers.ResetOthersPin) | ||||||
|  | 	ls.DbRs.AddLocalFunc("save_others_temporary_pin", appHandlers.SaveOthersTemporaryPin) | ||||||
|  | 	ls.DbRs.AddLocalFunc("get_current_profile_info", appHandlers.GetCurrentProfileInfo) | ||||||
|  | 	ls.DbRs.AddLocalFunc("check_transactions", appHandlers.CheckTransactions) | ||||||
|  | 	ls.DbRs.AddLocalFunc("get_transactions", appHandlers.GetTransactionsList) | ||||||
|  | 	ls.DbRs.AddLocalFunc("view_statement", appHandlers.ViewTransactionStatement) | ||||||
|  | 	ls.DbRs.AddLocalFunc("update_all_profile_items", appHandlers.UpdateAllProfileItems) | ||||||
|  | 	ls.DbRs.AddLocalFunc("set_back", appHandlers.SetBack) | ||||||
|  | 	ls.DbRs.AddLocalFunc("show_blocked_account", appHandlers.ShowBlockedAccount) | ||||||
|  | 
 | ||||||
|  | 	return appHandlers, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO: enable setting of sessionId on engine init time
 | ||||||
|  | func (ls *LocalHandlerService) GetEngine() *engine.DefaultEngine { | ||||||
|  | 	en := engine.NewEngine(ls.Cfg, ls.Rs) | ||||||
|  | 	en = en.WithPersister(ls.Pe) | ||||||
|  | 	return en | ||||||
|  | } | ||||||
| @ -1,130 +0,0 @@ | |||||||
| package handlers |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 
 |  | ||||||
| 	"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) { |  | ||||||
| 	ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService) |  | ||||||
| 	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.CheckPinMisMatch) |  | ||||||
| 	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) |  | ||||||
| 
 |  | ||||||
| 	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 |  | ||||||
| } |  | ||||||
| @ -6,9 +6,9 @@ import ( | |||||||
| 	"io" | 	"io" | ||||||
| 
 | 
 | ||||||
| 	"git.defalsify.org/vise.git/engine" | 	"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.defalsify.org/vise.git/logging" | ||||||
|  | 	"git.defalsify.org/vise.git/persist" | ||||||
|  | 	"git.defalsify.org/vise.git/resource" | ||||||
| 
 | 
 | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
| ) | ) | ||||||
| @ -39,7 +39,7 @@ type RequestSession struct { | |||||||
| 
 | 
 | ||||||
| // TODO: seems like can remove this.
 | // TODO: seems like can remove this.
 | ||||||
| type RequestParser interface { | type RequestParser interface { | ||||||
| 	GetSessionId(rq any) (string, error) | 	GetSessionId(context context.Context, rq any) (string, error) | ||||||
| 	GetInput(rq any) ([]byte, error) | 	GetInput(rq any) ([]byte, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										120
									
								
								internal/http/at/parse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								internal/http/at/parse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | |||||||
|  | package at | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"git.grassecon.net/urdt/ussd/common" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type ATRequestParser struct { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (arp *ATRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) { | ||||||
|  | 	rqv, ok := rq.(*http.Request) | ||||||
|  | 	if !ok { | ||||||
|  | 		logg.Warnf("got an invalid request", "req", rq) | ||||||
|  | 		return "", handlers.ErrInvalidRequest | ||||||
|  | 	} | ||||||
|  | 	// Capture body (if any) for logging
 | ||||||
|  | 	body, err := io.ReadAll(rqv.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logg.Warnf("failed to read request body", "err", err) | ||||||
|  | 		return "", fmt.Errorf("failed to read request body: %v", err) | ||||||
|  | 	} | ||||||
|  | 	// Reset the body for further reading
 | ||||||
|  | 	rqv.Body = io.NopCloser(bytes.NewReader(body)) | ||||||
|  | 
 | ||||||
|  | 	// Log the body as JSON
 | ||||||
|  | 	bodyLog := map[string]string{"body": string(body)} | ||||||
|  | 	logBytes, err := json.Marshal(bodyLog) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logg.Warnf("failed to marshal request body", "err", err) | ||||||
|  | 	} else { | ||||||
|  | 		decodedStr := string(logBytes) | ||||||
|  | 		sessionId, err := extractATSessionId(decodedStr) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx = context.WithValue(ctx, "AT-SessionId", sessionId) | ||||||
|  | 		} | ||||||
|  | 		logg.DebugCtxf(ctx, "Received request:", decodedStr) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := rqv.ParseForm(); err != nil { | ||||||
|  | 		logg.Warnf("failed to parse form data", "err", err) | ||||||
|  | 		return "", fmt.Errorf("failed to parse form data: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	phoneNumber := rqv.FormValue("phoneNumber") | ||||||
|  | 	if phoneNumber == "" { | ||||||
|  | 		return "", fmt.Errorf("no phone number found") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	formattedNumber, err := common.FormatPhoneNumber(phoneNumber) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logg.Warnf("failed to format phone number", "err", err) | ||||||
|  | 		return "", fmt.Errorf("failed to format number") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return formattedNumber, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (arp *ATRequestParser) GetInput(rq any) ([]byte, error) { | ||||||
|  | 	rqv, ok := rq.(*http.Request) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, handlers.ErrInvalidRequest | ||||||
|  | 	} | ||||||
|  | 	if err := rqv.ParseForm(); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to parse form data: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	text := rqv.FormValue("text") | ||||||
|  | 
 | ||||||
|  | 	parts := strings.Split(text, "*") | ||||||
|  | 	if len(parts) == 0 { | ||||||
|  | 		return nil, fmt.Errorf("no input found") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	trimmedInput := strings.TrimSpace(parts[len(parts)-1]) | ||||||
|  | 	return []byte(trimmedInput), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parseQueryParams(query string) map[string]string { | ||||||
|  | 	params := make(map[string]string) | ||||||
|  | 
 | ||||||
|  | 	queryParams := strings.Split(query, "&") | ||||||
|  | 	for _, param := range queryParams { | ||||||
|  | 		// Split each key-value pair by '='
 | ||||||
|  | 		parts := strings.SplitN(param, "=", 2) | ||||||
|  | 		if len(parts) == 2 { | ||||||
|  | 			params[parts[0]] = parts[1] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return params | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func extractATSessionId(decodedStr string) (string, error) { | ||||||
|  | 	var data map[string]string | ||||||
|  | 	err := json.Unmarshal([]byte(decodedStr), &data) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		logg.Errorf("Error unmarshalling JSON: %v", err) | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  | 	decodedBody, err := url.QueryUnescape(data["body"]) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logg.Errorf("Error URL-decoding body: %v", err) | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  | 	params := parseQueryParams(decodedBody) | ||||||
|  | 
 | ||||||
|  | 	sessionId := params["sessionId"] | ||||||
|  | 	return sessionId, nil | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -1,19 +1,25 @@ | |||||||
| package http | package at | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 
 | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
|  | 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	logg = logging.NewVanilla().WithDomain("atserver").WithContextKey("SessionId").WithContextKey("AT-SessionId") | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type ATSessionHandler struct { | type ATSessionHandler struct { | ||||||
| 	*SessionHandler | 	*httpserver.SessionHandler | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler { | func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler { | ||||||
| 	return &ATSessionHandler{ | 	return &ATSessionHandler{ | ||||||
| 		SessionHandler: ToSessionHandler(h), | 		SessionHandler: httpserver.ToSessionHandler(h), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -28,17 +34,17 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) | |||||||
| 
 | 
 | ||||||
| 	rp := ash.GetRequestParser() | 	rp := ash.GetRequestParser() | ||||||
| 	cfg := ash.GetConfig() | 	cfg := ash.GetConfig() | ||||||
| 	cfg.SessionId, err = rp.GetSessionId(req) | 	cfg.SessionId, err = rp.GetSessionId(req.Context(), req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||||
| 		ash.writeError(w, 400, err) | 		ash.WriteError(w, 400, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	rqs.Config = cfg | 	rqs.Config = cfg | ||||||
| 	rqs.Input, err = rp.GetInput(req) | 	rqs.Input, err = rp.GetInput(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||||
| 		ash.writeError(w, 400, err) | 		ash.WriteError(w, 400, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -53,7 +59,7 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if code != 200 { | 	if code != 200 { | ||||||
| 		ash.writeError(w, 500, err) | 		ash.WriteError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -61,13 +67,13 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) | |||||||
| 	w.Header().Set("Content-Type", "text/plain") | 	w.Header().Set("Content-Type", "text/plain") | ||||||
| 	rqs, err = ash.Output(rqs) | 	rqs, err = ash.Output(rqs) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ash.writeError(w, 500, err) | 		ash.WriteError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	rqs, err = ash.Reset(rqs) | 	rqs, err = ash.Reset(rqs) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ash.writeError(w, 500, err) | 		ash.WriteError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @ -1,7 +1,6 @@ | |||||||
| package http | package at | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"io" | 	"io" | ||||||
| @ -16,16 +15,6 @@ import ( | |||||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks" | 	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // invalidRequestType is a custom type to test invalid request scenarios
 |  | ||||||
| type invalidRequestType struct{} |  | ||||||
| 
 |  | ||||||
| // errorReader is a helper type that always returns an error when Read is called
 |  | ||||||
| type errorReader struct{} |  | ||||||
| 
 |  | ||||||
| func (e *errorReader) Read(p []byte) (n int, err error) { |  | ||||||
| 	return 0, errors.New("read error") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestNewATSessionHandler(t *testing.T) { | func TestNewATSessionHandler(t *testing.T) { | ||||||
| 	mockHandler := &httpmocks.MockRequestHandler{} | 	mockHandler := &httpmocks.MockRequestHandler{} | ||||||
| 	ash := NewATSessionHandler(mockHandler) | 	ash := NewATSessionHandler(mockHandler) | ||||||
| @ -242,208 +231,4 @@ func TestATSessionHandler_Output(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestSessionHandler_ServeHTTP(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name           string |  | ||||||
| 		sessionID      string |  | ||||||
| 		input          []byte |  | ||||||
| 		parserErr      error |  | ||||||
| 		processErr     error |  | ||||||
| 		outputErr      error |  | ||||||
| 		resetErr       error |  | ||||||
| 		expectedStatus int |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name:           "Success", |  | ||||||
| 			sessionID:      "123", |  | ||||||
| 			input:          []byte("test input"), |  | ||||||
| 			expectedStatus: http.StatusOK, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:           "Missing Session ID", |  | ||||||
| 			sessionID:      "", |  | ||||||
| 			parserErr:      handlers.ErrSessionMissing, |  | ||||||
| 			expectedStatus: http.StatusBadRequest, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:           "Process Error", |  | ||||||
| 			sessionID:      "123", |  | ||||||
| 			input:          []byte("test input"), |  | ||||||
| 			processErr:     handlers.ErrStorage, |  | ||||||
| 			expectedStatus: http.StatusInternalServerError, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:           "Output Error", |  | ||||||
| 			sessionID:      "123", |  | ||||||
| 			input:          []byte("test input"), |  | ||||||
| 			outputErr:      errors.New("output error"), |  | ||||||
| 			expectedStatus: http.StatusOK, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:           "Reset Error", |  | ||||||
| 			sessionID:      "123", |  | ||||||
| 			input:          []byte("test input"), |  | ||||||
| 			resetErr:       errors.New("reset error"), |  | ||||||
| 			expectedStatus: http.StatusOK, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			mockRequestParser := &httpmocks.MockRequestParser{ |  | ||||||
| 				GetSessionIdFunc: func(any) (string, error) { |  | ||||||
| 					return tt.sessionID, tt.parserErr |  | ||||||
| 				}, |  | ||||||
| 				GetInputFunc: func(any) ([]byte, error) { |  | ||||||
| 					return tt.input, nil |  | ||||||
| 				}, |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			mockRequestHandler := &httpmocks.MockRequestHandler{ |  | ||||||
| 				ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 					return rs, tt.processErr |  | ||||||
| 				}, |  | ||||||
| 				OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 					return rs, tt.outputErr |  | ||||||
| 				}, |  | ||||||
| 				ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { |  | ||||||
| 					return rs, tt.resetErr |  | ||||||
| 				}, |  | ||||||
| 				GetRequestParserFunc: func() handlers.RequestParser { |  | ||||||
| 					return mockRequestParser |  | ||||||
| 				}, |  | ||||||
| 				GetConfigFunc: func() engine.Config { |  | ||||||
| 					return engine.Config{} |  | ||||||
| 				}, |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			sessionHandler := ToSessionHandler(mockRequestHandler) |  | ||||||
| 
 |  | ||||||
| 			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input)) |  | ||||||
| 			req.Header.Set("X-Vise-Session", tt.sessionID) |  | ||||||
| 
 |  | ||||||
| 			rr := httptest.NewRecorder() |  | ||||||
| 
 |  | ||||||
| 			sessionHandler.ServeHTTP(rr, req) |  | ||||||
| 
 |  | ||||||
| 			if status := rr.Code; status != tt.expectedStatus { |  | ||||||
| 				t.Errorf("handler returned wrong status code: got %v want %v", |  | ||||||
| 					status, tt.expectedStatus) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestSessionHandler_writeError(t *testing.T) { |  | ||||||
| 	handler := &SessionHandler{} |  | ||||||
| 	mockWriter := &httpmocks.MockWriter{} |  | ||||||
| 	err := errors.New("test error") |  | ||||||
| 
 |  | ||||||
| 	handler.writeError(mockWriter, http.StatusBadRequest, err) |  | ||||||
| 
 |  | ||||||
| 	if mockWriter.WrittenString != "" { |  | ||||||
| 		t.Errorf("Expected empty body, got %s", mockWriter.WrittenString) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDefaultRequestParser_GetSessionId(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name          string |  | ||||||
| 		request       any |  | ||||||
| 		expectedID    string |  | ||||||
| 		expectedError error |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name: "Valid Session ID", |  | ||||||
| 			request: func() *http.Request { |  | ||||||
| 				req := httptest.NewRequest(http.MethodPost, "/", nil) |  | ||||||
| 				req.Header.Set("X-Vise-Session", "123456") |  | ||||||
| 				return req |  | ||||||
| 			}(), |  | ||||||
| 			expectedID:    "123456", |  | ||||||
| 			expectedError: nil, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:          "Missing Session ID", |  | ||||||
| 			request:       httptest.NewRequest(http.MethodPost, "/", nil), |  | ||||||
| 			expectedID:    "", |  | ||||||
| 			expectedError: handlers.ErrSessionMissing, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:          "Invalid Request Type", |  | ||||||
| 			request:       invalidRequestType{}, |  | ||||||
| 			expectedID:    "", |  | ||||||
| 			expectedError: handlers.ErrInvalidRequest, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	parser := &DefaultRequestParser{} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			id, err := parser.GetSessionId(tt.request) |  | ||||||
| 
 |  | ||||||
| 			if id != tt.expectedID { |  | ||||||
| 				t.Errorf("Expected session ID %s, got %s", tt.expectedID, id) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if err != tt.expectedError { |  | ||||||
| 				t.Errorf("Expected error %v, got %v", tt.expectedError, err) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func TestDefaultRequestParser_GetInput(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name          string |  | ||||||
| 		request       any |  | ||||||
| 		expectedInput []byte |  | ||||||
| 		expectedError error |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name: "Valid Input", |  | ||||||
| 			request: func() *http.Request { |  | ||||||
| 				return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input")) |  | ||||||
| 			}(), |  | ||||||
| 			expectedInput: []byte("test input"), |  | ||||||
| 			expectedError: nil, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:          "Empty Input", |  | ||||||
| 			request:       httptest.NewRequest(http.MethodPost, "/", nil), |  | ||||||
| 			expectedInput: []byte{}, |  | ||||||
| 			expectedError: nil, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:          "Invalid Request Type", |  | ||||||
| 			request:       invalidRequestType{}, |  | ||||||
| 			expectedInput: nil, |  | ||||||
| 			expectedError: handlers.ErrInvalidRequest, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Read Error", |  | ||||||
| 			request: func() *http.Request { |  | ||||||
| 				return httptest.NewRequest(http.MethodPost, "/", &errorReader{}) |  | ||||||
| 			}(), |  | ||||||
| 			expectedInput: nil, |  | ||||||
| 			expectedError: errors.New("read error"), |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	parser := &DefaultRequestParser{} |  | ||||||
| 
 |  | ||||||
| 	for _, tt := range tests { |  | ||||||
| 		t.Run(tt.name, func(t *testing.T) { |  | ||||||
| 			input, err := parser.GetInput(tt.request) |  | ||||||
| 
 |  | ||||||
| 			if !bytes.Equal(input, tt.expectedInput) { |  | ||||||
| 				t.Errorf("Expected input %s, got %s", tt.expectedInput, input) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) { |  | ||||||
| 				t.Errorf("Expected error %v, got %v", tt.expectedError, err) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										37
									
								
								internal/http/parse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								internal/http/parse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | package http | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type DefaultRequestParser struct { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) { | ||||||
|  | 	rqv, ok := rq.(*http.Request) | ||||||
|  | 	if !ok { | ||||||
|  | 		return "", handlers.ErrInvalidRequest | ||||||
|  | 	} | ||||||
|  | 	v := rqv.Header.Get("X-Vise-Session") | ||||||
|  | 	if v == "" { | ||||||
|  | 		return "", handlers.ErrSessionMissing | ||||||
|  | 	} | ||||||
|  | 	return v, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) { | ||||||
|  | 	rqv, ok := rq.(*http.Request) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, handlers.ErrInvalidRequest | ||||||
|  | 	} | ||||||
|  | 	defer rqv.Body.Close() | ||||||
|  | 	v, err := ioutil.ReadAll(rqv.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return v, nil | ||||||
|  | } | ||||||
| @ -1,7 +1,6 @@ | |||||||
| package http | package http | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| @ -14,35 +13,6 @@ var ( | |||||||
| 	logg = logging.NewVanilla().WithDomain("httpserver") | 	logg = logging.NewVanilla().WithDomain("httpserver") | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type DefaultRequestParser struct { |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| func(rp *DefaultRequestParser) GetSessionId(rq any) (string, error) { |  | ||||||
| 	rqv, ok := rq.(*http.Request) |  | ||||||
| 	if !ok { |  | ||||||
| 		return "", handlers.ErrInvalidRequest |  | ||||||
| 	} |  | ||||||
| 	v := rqv.Header.Get("X-Vise-Session") |  | ||||||
| 	if v == "" { |  | ||||||
| 		return "", handlers.ErrSessionMissing |  | ||||||
| 	} |  | ||||||
| 	return v, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func(rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) { |  | ||||||
| 	rqv, ok := rq.(*http.Request) |  | ||||||
| 	if !ok { |  | ||||||
| 		return nil, handlers.ErrInvalidRequest |  | ||||||
| 	} |  | ||||||
| 	defer rqv.Body.Close() |  | ||||||
| 	v, err := ioutil.ReadAll(rqv.Body) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return v, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type SessionHandler struct { | type SessionHandler struct { | ||||||
| 	handlers.RequestHandler | 	handlers.RequestHandler | ||||||
| } | } | ||||||
| @ -53,19 +23,18 @@ func ToSessionHandler(h handlers.RequestHandler) *SessionHandler { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) { | func (f *SessionHandler) WriteError(w http.ResponseWriter, code int, err error) { | ||||||
| 	s := err.Error() | 	s := err.Error() | ||||||
| 	w.Header().Set("Content-Length", strconv.Itoa(len(s))) | 	w.Header().Set("Content-Length", strconv.Itoa(len(s))) | ||||||
| 	w.WriteHeader(code) | 	w.WriteHeader(code) | ||||||
| 	_, err = w.Write([]byte{}) | 	_, err = w.Write([]byte(s)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.Errorf("error writing error!!", "err", err, "olderr", s) | 		logg.Errorf("error writing error!!", "err", err, "olderr", s) | ||||||
| 		w.WriteHeader(500) | 		w.WriteHeader(500) | ||||||
| 	} | 	} | ||||||
| 	return  |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||||
| 	var code int | 	var code int | ||||||
| 	var err error | 	var err error | ||||||
| 	var perr error | 	var perr error | ||||||
| @ -77,16 +46,16 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |||||||
| 
 | 
 | ||||||
| 	rp := f.GetRequestParser() | 	rp := f.GetRequestParser() | ||||||
| 	cfg := f.GetConfig() | 	cfg := f.GetConfig() | ||||||
| 	cfg.SessionId, err = rp.GetSessionId(req) | 	cfg.SessionId, err = rp.GetSessionId(req.Context(), req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||||
| 		f.writeError(w, 400, err) | 		f.WriteError(w, 400, err) | ||||||
| 	} | 	} | ||||||
| 	rqs.Config = cfg | 	rqs.Config = cfg | ||||||
| 	rqs.Input, err = rp.GetInput(req) | 	rqs.Input, err = rp.GetInput(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | 		logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err) | ||||||
| 		f.writeError(w, 400, err) | 		f.WriteError(w, 400, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -103,7 +72,7 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if code != 200 { | 	if code != 200 { | ||||||
| 		f.writeError(w, 500, err) | 		f.WriteError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -112,11 +81,11 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |||||||
| 	rqs, err = f.Output(rqs) | 	rqs, err = f.Output(rqs) | ||||||
| 	rqs, perr = f.Reset(rqs) | 	rqs, perr = f.Reset(rqs) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		f.writeError(w, 500, err) | 		f.WriteError(w, 500, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if perr != nil { | 	if perr != nil { | ||||||
| 		f.writeError(w, 500, perr) | 		f.WriteError(w, 500, perr) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										230
									
								
								internal/http/server_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								internal/http/server_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,230 @@ | |||||||
|  | package http | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/engine" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // invalidRequestType is a custom type to test invalid request scenarios
 | ||||||
|  | type invalidRequestType struct{} | ||||||
|  | 
 | ||||||
|  | // errorReader is a helper type that always returns an error when Read is called
 | ||||||
|  | type errorReader struct{} | ||||||
|  | 
 | ||||||
|  | func (e *errorReader) Read(p []byte) (n int, err error) { | ||||||
|  | 	return 0, errors.New("read error") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestSessionHandler_ServeHTTP(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name           string | ||||||
|  | 		sessionID      string | ||||||
|  | 		input          []byte | ||||||
|  | 		parserErr      error | ||||||
|  | 		processErr     error | ||||||
|  | 		outputErr      error | ||||||
|  | 		resetErr       error | ||||||
|  | 		expectedStatus int | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:           "Success", | ||||||
|  | 			sessionID:      "123", | ||||||
|  | 			input:          []byte("test input"), | ||||||
|  | 			expectedStatus: http.StatusOK, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:           "Missing Session ID", | ||||||
|  | 			sessionID:      "", | ||||||
|  | 			parserErr:      handlers.ErrSessionMissing, | ||||||
|  | 			expectedStatus: http.StatusBadRequest, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:           "Process Error", | ||||||
|  | 			sessionID:      "123", | ||||||
|  | 			input:          []byte("test input"), | ||||||
|  | 			processErr:     handlers.ErrStorage, | ||||||
|  | 			expectedStatus: http.StatusInternalServerError, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:           "Output Error", | ||||||
|  | 			sessionID:      "123", | ||||||
|  | 			input:          []byte("test input"), | ||||||
|  | 			outputErr:      errors.New("output error"), | ||||||
|  | 			expectedStatus: http.StatusOK, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:           "Reset Error", | ||||||
|  | 			sessionID:      "123", | ||||||
|  | 			input:          []byte("test input"), | ||||||
|  | 			resetErr:       errors.New("reset error"), | ||||||
|  | 			expectedStatus: http.StatusOK, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			mockRequestParser := &httpmocks.MockRequestParser{ | ||||||
|  | 				GetSessionIdFunc: func(any) (string, error) { | ||||||
|  | 					return tt.sessionID, tt.parserErr | ||||||
|  | 				}, | ||||||
|  | 				GetInputFunc: func(any) ([]byte, error) { | ||||||
|  | 					return tt.input, nil | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			mockRequestHandler := &httpmocks.MockRequestHandler{ | ||||||
|  | 				ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { | ||||||
|  | 					return rs, tt.processErr | ||||||
|  | 				}, | ||||||
|  | 				OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { | ||||||
|  | 					return rs, tt.outputErr | ||||||
|  | 				}, | ||||||
|  | 				ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) { | ||||||
|  | 					return rs, tt.resetErr | ||||||
|  | 				}, | ||||||
|  | 				GetRequestParserFunc: func() handlers.RequestParser { | ||||||
|  | 					return mockRequestParser | ||||||
|  | 				}, | ||||||
|  | 				GetConfigFunc: func() engine.Config { | ||||||
|  | 					return engine.Config{} | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			sessionHandler := ToSessionHandler(mockRequestHandler) | ||||||
|  | 
 | ||||||
|  | 			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input)) | ||||||
|  | 			req.Header.Set("X-Vise-Session", tt.sessionID) | ||||||
|  | 
 | ||||||
|  | 			rr := httptest.NewRecorder() | ||||||
|  | 
 | ||||||
|  | 			sessionHandler.ServeHTTP(rr, req) | ||||||
|  | 
 | ||||||
|  | 			if status := rr.Code; status != tt.expectedStatus { | ||||||
|  | 				t.Errorf("handler returned wrong status code: got %v want %v", | ||||||
|  | 					status, tt.expectedStatus) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestSessionHandler_WriteError(t *testing.T) { | ||||||
|  | 	handler := &SessionHandler{} | ||||||
|  | 	mockWriter := &httpmocks.MockWriter{} | ||||||
|  | 	err := errors.New("test error") | ||||||
|  | 
 | ||||||
|  | 	handler.WriteError(mockWriter, http.StatusBadRequest, err) | ||||||
|  | 
 | ||||||
|  | 	if mockWriter.WrittenString != "" { | ||||||
|  | 		t.Errorf("Expected empty body, got %s", mockWriter.WrittenString) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestDefaultRequestParser_GetSessionId(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name          string | ||||||
|  | 		request       any | ||||||
|  | 		expectedID    string | ||||||
|  | 		expectedError error | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "Valid Session ID", | ||||||
|  | 			request: func() *http.Request { | ||||||
|  | 				req := httptest.NewRequest(http.MethodPost, "/", nil) | ||||||
|  | 				req.Header.Set("X-Vise-Session", "123456") | ||||||
|  | 				return req | ||||||
|  | 			}(), | ||||||
|  | 			expectedID:    "123456", | ||||||
|  | 			expectedError: nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:          "Missing Session ID", | ||||||
|  | 			request:       httptest.NewRequest(http.MethodPost, "/", nil), | ||||||
|  | 			expectedID:    "", | ||||||
|  | 			expectedError: handlers.ErrSessionMissing, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:          "Invalid Request Type", | ||||||
|  | 			request:       invalidRequestType{}, | ||||||
|  | 			expectedID:    "", | ||||||
|  | 			expectedError: handlers.ErrInvalidRequest, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	parser := &DefaultRequestParser{} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			id, err := parser.GetSessionId(context.Background(),tt.request) | ||||||
|  | 
 | ||||||
|  | 			if id != tt.expectedID { | ||||||
|  | 				t.Errorf("Expected session ID %s, got %s", tt.expectedID, id) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if err != tt.expectedError { | ||||||
|  | 				t.Errorf("Expected error %v, got %v", tt.expectedError, err) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestDefaultRequestParser_GetInput(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name          string | ||||||
|  | 		request       any | ||||||
|  | 		expectedInput []byte | ||||||
|  | 		expectedError error | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "Valid Input", | ||||||
|  | 			request: func() *http.Request { | ||||||
|  | 				return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input")) | ||||||
|  | 			}(), | ||||||
|  | 			expectedInput: []byte("test input"), | ||||||
|  | 			expectedError: nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:          "Empty Input", | ||||||
|  | 			request:       httptest.NewRequest(http.MethodPost, "/", nil), | ||||||
|  | 			expectedInput: []byte{}, | ||||||
|  | 			expectedError: nil, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:          "Invalid Request Type", | ||||||
|  | 			request:       invalidRequestType{}, | ||||||
|  | 			expectedInput: nil, | ||||||
|  | 			expectedError: handlers.ErrInvalidRequest, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Read Error", | ||||||
|  | 			request: func() *http.Request { | ||||||
|  | 				return httptest.NewRequest(http.MethodPost, "/", &errorReader{}) | ||||||
|  | 			}(), | ||||||
|  | 			expectedInput: nil, | ||||||
|  | 			expectedError: errors.New("read error"), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	parser := &DefaultRequestParser{} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			input, err := parser.GetInput(tt.request) | ||||||
|  | 
 | ||||||
|  | 			if !bytes.Equal(input, tt.expectedInput) { | ||||||
|  | 				t.Errorf("Expected input %s, got %s", tt.expectedInput, input) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) { | ||||||
|  | 				t.Errorf("Expected error %v, got %v", tt.expectedError, err) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								internal/ssh/keystore.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								internal/ssh/keystore.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | package ssh | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 
 | ||||||
|  | 	"golang.org/x/crypto/ssh" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/db" | ||||||
|  | 
 | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
|  | 	dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type SshKeyStore struct { | ||||||
|  | 	store db.Db | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewSshKeyStore(ctx context.Context, dbDir string) (*SshKeyStore, error) { | ||||||
|  | 	keyStore := &SshKeyStore{} | ||||||
|  | 	keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm") | ||||||
|  | 	keyStore.store = dbstorage.NewThreadGdbmDb() | ||||||
|  | 	err := keyStore.store.Connect(ctx, keyStoreFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return keyStore, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(s *SshKeyStore) AddFromFile(ctx context.Context, fp string, sessionId string) error { | ||||||
|  | 	_, err := os.Stat(fp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("cannot open ssh server public key file: %v\n", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	publicBytes, err := os.ReadFile(fp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("Failed to load public key: %v", err) | ||||||
|  | 	} | ||||||
|  | 	pubKey, _, _, _, err := ssh.ParseAuthorizedKey(publicBytes) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("Failed to parse public key: %v", err) | ||||||
|  | 	} | ||||||
|  | 	k := append([]byte{0x01}, pubKey.Marshal()...) | ||||||
|  | 	s.store.SetPrefix(storage.DATATYPE_EXTEND) | ||||||
|  | 	logg.Infof("Added key", "sessionId", sessionId, "public key", string(publicBytes)) | ||||||
|  | 	return s.store.Put(ctx, k, []byte(sessionId)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(s *SshKeyStore) Get(ctx context.Context, pubKey ssh.PublicKey) (string, error) { | ||||||
|  | 	s.store.SetLanguage(nil) | ||||||
|  | 	s.store.SetPrefix(storage.DATATYPE_EXTEND) | ||||||
|  | 	k := append([]byte{0x01}, pubKey.Marshal()...) | ||||||
|  | 	v, err := s.store.Get(ctx, k) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return string(v), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(s *SshKeyStore) Close() error { | ||||||
|  | 	return s.store.Close() | ||||||
|  | } | ||||||
							
								
								
									
										284
									
								
								internal/ssh/ssh.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								internal/ssh/ssh.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,284 @@ | |||||||
|  | package ssh | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"sync" | ||||||
|  | 
 | ||||||
|  | 	"golang.org/x/crypto/ssh" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/engine" | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
|  | 	"git.defalsify.org/vise.git/resource" | ||||||
|  | 	"git.defalsify.org/vise.git/state" | ||||||
|  | 
 | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/remote" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	logg = logging.NewVanilla().WithDomain("ssh") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type auther struct { | ||||||
|  | 	Ctx context.Context | ||||||
|  | 	keyStore *SshKeyStore | ||||||
|  | 	auth map[string]string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther { | ||||||
|  | 	return &auther{ | ||||||
|  | 		Ctx: ctx, | ||||||
|  | 		keyStore: keyStore, | ||||||
|  | 		auth: make(map[string]string), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { | ||||||
|  | 	logg.TraceCtxf(a.Ctx, "looking for publickey", "pubkey", fmt.Sprintf("%x", pubKey)) | ||||||
|  | 	va, err := a.keyStore.Get(a.Ctx, pubKey) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	ka := hex.EncodeToString(conn.SessionID()) | ||||||
|  | 	a.auth[ka] = va  | ||||||
|  | 	fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va) | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(a *auther) FromConn(c *ssh.ServerConn) (string, error) { | ||||||
|  | 	if c == nil { | ||||||
|  | 		return "", errors.New("nil server conn") | ||||||
|  | 	} | ||||||
|  | 	if c.Conn == nil { | ||||||
|  | 		return "", errors.New("nil underlying conn") | ||||||
|  | 	} | ||||||
|  | 	return a.Get(c.Conn.SessionID()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | func(a *auther) Get(k []byte) (string, error) { | ||||||
|  | 	ka := hex.EncodeToString(k) | ||||||
|  | 	v, ok := a.auth[ka] | ||||||
|  | 	if !ok { | ||||||
|  | 		return "", errors.New("not found") | ||||||
|  | 	} | ||||||
|  | 	return v, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type SshRunner struct { | ||||||
|  | 	Ctx context.Context | ||||||
|  | 	Cfg engine.Config | ||||||
|  | 	FlagFile string | ||||||
|  | 	Conn storage.ConnData | ||||||
|  | 	ResourceDir string | ||||||
|  | 	Debug bool | ||||||
|  | 	SrvKeyFile string | ||||||
|  | 	Host string | ||||||
|  | 	Port uint | ||||||
|  | 	wg sync.WaitGroup | ||||||
|  | 	lst net.Listener | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error { | ||||||
|  | 	if ch == nil { | ||||||
|  | 		return errors.New("nil channel") | ||||||
|  | 	} | ||||||
|  | 	if ch.ChannelType() != "session" { | ||||||
|  | 		ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for") | ||||||
|  | 		return errors.New("not a session") | ||||||
|  | 	} | ||||||
|  | 	channel, requests, err := ch.Accept() | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	defer channel.Close() | ||||||
|  | 	s.wg.Add(1) | ||||||
|  | 	go func(reqIn <-chan *ssh.Request) { | ||||||
|  | 		defer s.wg.Done() | ||||||
|  | 		for req := range reqIn { | ||||||
|  | 			req.Reply(req.Type == "shell", nil)	 | ||||||
|  | 		} | ||||||
|  | 		_ = requests | ||||||
|  | 	}(requests) | ||||||
|  | 
 | ||||||
|  | 	cont, err := en.Exec(ctx, []byte{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("initial engine exec err: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var input [state.INPUT_LIMIT]byte | ||||||
|  | 	for cont { | ||||||
|  | 		c, err := en.Flush(ctx, channel) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("flush err: %v", err) | ||||||
|  | 		} | ||||||
|  | 		_, err = channel.Write([]byte{0x0a}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("newline err: %v", err) | ||||||
|  | 		} | ||||||
|  | 		c, err = channel.Read(input[:]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("read input fail: %v", err) | ||||||
|  | 		} | ||||||
|  | 		logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1]) | ||||||
|  | 		cont, err = en.Exec(ctx, input[:c-1]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("engine exec err: %v", err) | ||||||
|  | 		} | ||||||
|  | 		logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en) | ||||||
|  | 		_ = c | ||||||
|  | 	} | ||||||
|  | 	c, err := en.Flush(ctx, channel) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("last flush err: %v", err) | ||||||
|  | 	} | ||||||
|  | 	_ = c | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(s *SshRunner) Stop() error { | ||||||
|  | 	return s.lst.Close() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) { | ||||||
|  | 	ctx := s.Ctx | ||||||
|  | 	menuStorageService := storage.NewMenuStorageService(s.Conn, s.ResourceDir) | ||||||
|  | 
 | ||||||
|  | 	rs, err := menuStorageService.GetResource(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pe, err := menuStorageService.GetPersister(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	userdatastore, err := menuStorageService.GetUserdataDb(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dbResource, ok := rs.(*resource.DbResource) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	lhs, err := handlers.NewLocalHandlerService(ctx, s.FlagFile, true, dbResource, s.Cfg, rs) | ||||||
|  | 	lhs.SetDataStore(&userdatastore) | ||||||
|  | 	lhs.SetPersister(pe) | ||||||
|  | 	lhs.Cfg.SessionId = sessionId | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// TODO: clear up why pointer here and by-value other cmds
 | ||||||
|  | 	accountService := &remote.AccountService{} | ||||||
|  | 	hl, err := lhs.GetHandler(accountService) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	en := lhs.GetEngine() | ||||||
|  | 	en = en.WithFirst(hl.Init) | ||||||
|  | 	if s.Debug { | ||||||
|  | 		en = en.WithDebug(nil) | ||||||
|  | 	} | ||||||
|  | 	// TODO: this is getting very hacky!
 | ||||||
|  | 	closer := func() { | ||||||
|  | 		err := menuStorageService.Close() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return en, closer, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // adapted example from crypto/ssh package, NewServerConn doc
 | ||||||
|  | func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) { | ||||||
|  | 	s.Ctx = ctx | ||||||
|  | 	running := true | ||||||
|  | 
 | ||||||
|  | 	// TODO: waitgroup should probably not be global
 | ||||||
|  | 	defer s.wg.Wait() | ||||||
|  | 
 | ||||||
|  | 	auth := NewAuther(ctx, keyStore) | ||||||
|  | 	cfg := ssh.ServerConfig{ | ||||||
|  | 		PublicKeyCallback: auth.Check, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	privateBytes, err := os.ReadFile(s.SrvKeyFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logg.ErrorCtxf(ctx, "Failed to load private key", "err", err) | ||||||
|  | 	} | ||||||
|  | 	private, err := ssh.ParsePrivateKey(privateBytes) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err) | ||||||
|  | 	} | ||||||
|  | 	srvPub := private.PublicKey() | ||||||
|  | 	srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal()) | ||||||
|  | 	logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr) | ||||||
|  | 	cfg.AddHostKey(private) | ||||||
|  | 
 | ||||||
|  | 	s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for running { | ||||||
|  | 		conn, err := s.lst.Accept() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logg.ErrorCtxf(ctx, "ssh accept error", "err", err) | ||||||
|  | 			running = false | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		go func(conn net.Conn) { | ||||||
|  | 			defer conn.Close() | ||||||
|  | 			for true { | ||||||
|  | 				srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg) | ||||||
|  | 				if err != nil { | ||||||
|  | 					logg.InfoCtxf(ctx, "rejected client", "err", err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn) | ||||||
|  | 
 | ||||||
|  | 				s.wg.Add(1) | ||||||
|  | 				go func() { | ||||||
|  | 					ssh.DiscardRequests(rC) | ||||||
|  | 					s.wg.Done() | ||||||
|  | 				}() | ||||||
|  | 				 | ||||||
|  | 				sessionId, err := auth.FromConn(srvConn) | ||||||
|  | 				if err != nil { | ||||||
|  | 					logg.ErrorCtxf(ctx, "Cannot find authentication") | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				en, closer, err := s.GetEngine(sessionId) | ||||||
|  | 				if err != nil { | ||||||
|  | 					logg.ErrorCtxf(ctx, "engine won't start", "err", err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				defer func() { | ||||||
|  | 					err := en.Finish() | ||||||
|  | 					if err != nil { | ||||||
|  | 						logg.ErrorCtxf(ctx, "engine won't stop", "err", err) | ||||||
|  | 					} | ||||||
|  | 					closer() | ||||||
|  | 				}() | ||||||
|  | 				for ch := range nC { | ||||||
|  | 					err = s.serve(ctx, sessionId, ch, en) | ||||||
|  | 					logg.ErrorCtxf(ctx, "ssh server finish", "err", err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}(conn) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -4,8 +4,13 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 
 | 
 | ||||||
| 	"git.defalsify.org/vise.git/db" | 	"git.defalsify.org/vise.git/db" | ||||||
| 	"git.defalsify.org/vise.git/lang" |  | ||||||
| 	gdbmdb "git.defalsify.org/vise.git/db/gdbm" | 	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 ( | var ( | ||||||
| @ -114,3 +119,9 @@ func(tdb *ThreadGdbmDb) Close() error { | |||||||
| 	tdb.db = nil | 	tdb.db = nil | ||||||
| 	return err | 	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) | ||||||
|  | } | ||||||
| @ -6,10 +6,6 @@ import ( | |||||||
| 	"git.defalsify.org/vise.git/db" | 	"git.defalsify.org/vise.git/db" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( |  | ||||||
| 	DATATYPE_USERSUB = 64 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // PrefixDb interface abstracts the database operations.
 | // PrefixDb interface abstracts the database operations.
 | ||||||
| type PrefixDb interface { | type PrefixDb interface { | ||||||
| 	Get(ctx context.Context, key []byte) ([]byte, error) | 	Get(ctx context.Context, key []byte) ([]byte, error) | ||||||
| @ -35,13 +31,13 @@ func (s *SubPrefixDb) toKey(k []byte) []byte { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { | func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { | ||||||
| 	s.store.SetPrefix(DATATYPE_USERSUB) | 	s.store.SetPrefix(db.DATATYPE_USERDATA) | ||||||
| 	key = s.toKey(key) | 	key = s.toKey(key) | ||||||
| 	return s.store.Get(ctx, key) | 	return s.store.Get(ctx, key) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { | func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { | ||||||
| 	s.store.SetPrefix(DATATYPE_USERSUB) | 	s.store.SetPrefix(db.DATATYPE_USERDATA) | ||||||
| 	key = s.toKey(key) | 	key = s.toKey(key) | ||||||
| 	return s.store.Put(ctx, key, val) | 	return s.store.Put(ctx, key, val) | ||||||
| } | } | ||||||
							
								
								
									
										86
									
								
								internal/storage/parse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								internal/storage/parse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,86 @@ | |||||||
|  | package storage | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/url" | ||||||
|  | 	"path" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	DBTYPE_MEM = iota | ||||||
|  | 	DBTYPE_GDBM | ||||||
|  | 	DBTYPE_POSTGRES | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type ConnData struct { | ||||||
|  | 	typ int | ||||||
|  | 	str string | ||||||
|  | 	domain string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (cd *ConnData) DbType() int { | ||||||
|  | 	return cd.typ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (cd *ConnData) String() string { | ||||||
|  | 	return cd.str | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (cd *ConnData) Domain() string { | ||||||
|  | 	return cd.domain | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (cd *ConnData) Path() string { | ||||||
|  | 	v, _ := url.Parse(cd.str) | ||||||
|  | 	v.RawQuery = "" | ||||||
|  | 	return v.String() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func probePostgres(s string) (string, string, bool) { | ||||||
|  | 	domain := "public" | ||||||
|  | 	v, err := url.Parse(s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", "", false | ||||||
|  | 	} | ||||||
|  | 	if v.Scheme != "postgres" { | ||||||
|  | 		return "", "", false | ||||||
|  | 	} | ||||||
|  | 	vv := v.Query() | ||||||
|  | 	if vv.Has("search_path") { | ||||||
|  | 		domain = vv.Get("search_path") | ||||||
|  | 	} | ||||||
|  | 	return s, domain, true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func probeGdbm(s string) (string, string, bool) { | ||||||
|  | 	if !path.IsAbs(s) { | ||||||
|  | 		return "", "", false | ||||||
|  | 	} | ||||||
|  | 	s = path.Clean(s) | ||||||
|  | 	return s, "", true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ToConnData(connStr string) (ConnData, error) { | ||||||
|  | 	var o ConnData | ||||||
|  | 
 | ||||||
|  | 	if connStr == "" { | ||||||
|  | 		return o, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	v, domain, ok := probePostgres(connStr) | ||||||
|  | 	if ok { | ||||||
|  | 		o.typ = DBTYPE_POSTGRES | ||||||
|  | 		o.str = v | ||||||
|  | 		o.domain = domain | ||||||
|  | 		return o, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	v, _, ok = probeGdbm(connStr) | ||||||
|  | 	if ok { | ||||||
|  | 		o.typ = DBTYPE_GDBM | ||||||
|  | 		o.str = v | ||||||
|  | 		return o, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return o, fmt.Errorf("invalid connection string: %s", connStr) | ||||||
|  | } | ||||||
							
								
								
									
										28
									
								
								internal/storage/parse_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								internal/storage/parse_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | package storage | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestParseConnStr(t *testing.T) { | ||||||
|  | 	_, err := ToConnData("postgres://foo:bar@localhost:5432/baz") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err)	 | ||||||
|  | 	} | ||||||
|  | 	_, err = ToConnData("/foo/bar") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err)	 | ||||||
|  | 	} | ||||||
|  | 	_, err = ToConnData("/foo/bar/") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err)	 | ||||||
|  | 	} | ||||||
|  | 	_, err = ToConnData("foo/bar") | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("expected error") | ||||||
|  | 	} | ||||||
|  | 	_, err = ToConnData("http://foo/bar") | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("expected error") | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -5,6 +5,10 @@ import ( | |||||||
| 	"git.defalsify.org/vise.git/persist" | 	"git.defalsify.org/vise.git/persist" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | const ( | ||||||
|  | 	DATATYPE_EXTEND = 128 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| type Storage struct { | type Storage struct { | ||||||
| 	Persister *persist.Persister | 	Persister *persist.Persister | ||||||
| 	UserdataDb db.Db	 | 	UserdataDb db.Db	 | ||||||
|  | |||||||
| @ -9,10 +9,12 @@ import ( | |||||||
| 	"git.defalsify.org/vise.git/db" | 	"git.defalsify.org/vise.git/db" | ||||||
| 	fsdb "git.defalsify.org/vise.git/db/fs" | 	fsdb "git.defalsify.org/vise.git/db/fs" | ||||||
| 	"git.defalsify.org/vise.git/db/postgres" | 	"git.defalsify.org/vise.git/db/postgres" | ||||||
|  | 	"git.defalsify.org/vise.git/lang" | ||||||
| 	"git.defalsify.org/vise.git/logging" | 	"git.defalsify.org/vise.git/logging" | ||||||
| 	"git.defalsify.org/vise.git/persist" | 	"git.defalsify.org/vise.git/persist" | ||||||
| 	"git.defalsify.org/vise.git/resource" | 	"git.defalsify.org/vise.git/resource" | ||||||
| 	"git.grassecon.net/urdt/ussd/initializers" | 	gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm" | ||||||
|  | 	"github.com/jackc/pgx/v5/pgxpool" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| @ -23,60 +25,54 @@ type StorageService interface { | |||||||
| 	GetPersister(ctx context.Context) (*persist.Persister, error) | 	GetPersister(ctx context.Context) (*persist.Persister, error) | ||||||
| 	GetUserdataDb(ctx context.Context) db.Db | 	GetUserdataDb(ctx context.Context) db.Db | ||||||
| 	GetResource(ctx context.Context) (resource.Resource, error) | 	GetResource(ctx context.Context) (resource.Resource, error) | ||||||
| 	EnsureDbDir() error |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type MenuStorageService struct { | type MenuStorageService struct { | ||||||
| 	dbDir         string | 	conn ConnData | ||||||
| 	resourceDir   string | 	resourceDir   string | ||||||
|  | 	poResource    resource.Resource | ||||||
| 	resourceStore db.Db | 	resourceStore db.Db | ||||||
| 	stateStore    db.Db | 	stateStore    db.Db | ||||||
| 	userDataStore db.Db | 	userDataStore db.Db | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func buildConnStr() string { | func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageService { | ||||||
| 	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") |  | ||||||
| 
 |  | ||||||
| 	return fmt.Sprintf( |  | ||||||
| 		"postgres://%s:%s@%s:%s/%s", |  | ||||||
| 		user, password, host, port, dbName, |  | ||||||
| 	) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { |  | ||||||
| 	return &MenuStorageService{ | 	return &MenuStorageService{ | ||||||
| 		dbDir:       dbDir, | 		conn: conn, | ||||||
| 		resourceDir: resourceDir, | 		resourceDir: resourceDir, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) { | func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) { | ||||||
| 	database, ok := ctx.Value("Database").(string) | 	var newDb db.Db | ||||||
| 	if !ok { | 	var err error | ||||||
| 		return nil, fmt.Errorf("failed to select the database") |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	if existingDb != nil { | 	if existingDb != nil { | ||||||
| 		return existingDb, nil | 		return existingDb, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var newDb db.Db |  | ||||||
| 	var err error |  | ||||||
| 
 | 
 | ||||||
| 	if database == "postgres" { | 	connStr := ms.conn.String() | ||||||
| 		newDb = postgres.NewPgDb() | 	dbTyp := ms.conn.DbType() | ||||||
| 		connStr := buildConnStr() | 	if dbTyp == DBTYPE_POSTGRES { | ||||||
| 		err = newDb.Connect(ctx, connStr) | 		// TODO: move to vise
 | ||||||
| 	} else { | 		err = ensureSchemaExists(ctx, ms.conn) | ||||||
| 		newDb = NewThreadGdbmDb() | 		if err != nil { | ||||||
| 		storeFile := path.Join(ms.dbDir, fileName) | 			return nil, err | ||||||
| 		err = newDb.Connect(ctx, storeFile) |  | ||||||
| 		} | 		} | ||||||
| 
 | 		newDb = postgres.NewPgDb().WithSchema(ms.conn.Domain()) | ||||||
|  | 	} else if dbTyp == DBTYPE_GDBM { | ||||||
|  | 		err = ms.ensureDbDir() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		connStr = path.Join(connStr, section) | ||||||
|  | 		newDb = gdbmstorage.NewThreadGdbmDb() | ||||||
|  | 	} else { | ||||||
|  | 		return nil, fmt.Errorf("unsupported connection string: '%s'\n", ms.conn.String()) | ||||||
|  | 	} | ||||||
|  | 	logg.DebugCtxf(ctx, "connecting to db", "conn", connStr, "conndata", ms.conn) | ||||||
|  | 	err = newDb.Connect(ctx, connStr) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| @ -84,6 +80,45 @@ func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.D | |||||||
| 	return newDb, nil | 	return newDb, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // WithGettext triggers use of gettext for translation of templates and menus.
 | ||||||
|  | //
 | ||||||
|  | // The first language in `lns` will be used as default language, to resolve node keys to 
 | ||||||
|  | // language strings.
 | ||||||
|  | //
 | ||||||
|  | // If `lns` is an empty array, gettext will not be used.
 | ||||||
|  | func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *MenuStorageService { | ||||||
|  | 	if len(lns) == 0 { | ||||||
|  | 		logg.Warnf("Gettext requested but no languages supplied") | ||||||
|  | 		return ms | ||||||
|  | 	} | ||||||
|  | 	rs := resource.NewPoResource(lns[0], path) | ||||||
|  | 
 | ||||||
|  | 	for _, ln := range(lns) { | ||||||
|  | 		rs = rs.WithLanguage(ln) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ms.poResource = rs | ||||||
|  | 
 | ||||||
|  | 	return ms | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ensureSchemaExists creates a new schema if it does not exist
 | ||||||
|  | func ensureSchemaExists(ctx context.Context, conn ConnData) error { | ||||||
|  | 	h, err := pgxpool.New(ctx, conn.Path()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to connect to the database: %w", err) | ||||||
|  | 	} | ||||||
|  | 	defer h.Close() | ||||||
|  | 
 | ||||||
|  | 	query := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", conn.Domain()) | ||||||
|  | 	_, err = h.Exec(ctx, query) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to create schema: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { | func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { | ||||||
| 	stateStore, err := ms.GetStateStore(ctx) | 	stateStore, err := ms.GetStateStore(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -116,6 +151,11 @@ func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resourc | |||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	rfs := resource.NewDbResource(ms.resourceStore) | 	rfs := resource.NewDbResource(ms.resourceStore) | ||||||
|  | 	if ms.poResource != nil { | ||||||
|  | 		logg.InfoCtxf(ctx, "using poresource for menu and template") | ||||||
|  | 		rfs.WithMenuGetter(ms.poResource.GetMenu) | ||||||
|  | 		rfs.WithTemplateGetter(ms.poResource.GetTemplate) | ||||||
|  | 	} | ||||||
| 	return rfs, nil | 	return rfs, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -133,8 +173,8 @@ func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) | |||||||
| 	return ms.stateStore, nil | 	return ms.stateStore, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ms *MenuStorageService) EnsureDbDir() error { | func (ms *MenuStorageService) ensureDbDir() error { | ||||||
| 	err := os.MkdirAll(ms.dbDir, 0700) | 	err := os.MkdirAll(ms.conn.String(), 0700) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("state dir create exited with error: %v\n", err) | 		return fmt.Errorf("state dir create exited with error: %v\n", err) | ||||||
| 	} | 	} | ||||||
| @ -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 |  | ||||||
| } |  | ||||||
							
								
								
									
										209
									
								
								internal/testutil/engine.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								internal/testutil/engine.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,209 @@ | |||||||
|  | package testutil | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"net/url" | ||||||
|  | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"git.defalsify.org/vise.git/engine" | ||||||
|  | 	"git.defalsify.org/vise.git/logging" | ||||||
|  | 	"git.defalsify.org/vise.git/resource" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/config" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/initializers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/testutil/testservice" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/internal/testutil/testtag" | ||||||
|  | 	"git.grassecon.net/urdt/ussd/remote" | ||||||
|  | 	"github.com/jackc/pgx/v5/pgxpool" | ||||||
|  | 	testdataloader "github.com/peteole/testdata-loader" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	logg        = logging.NewVanilla() | ||||||
|  | 	baseDir     = testdataloader.GetBasePath() | ||||||
|  | 	scriptDir   = path.Join(baseDir, "services", "registration") | ||||||
|  | 	setDbType   string | ||||||
|  | 	setConnStr  string | ||||||
|  | 	setDbSchema string | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	initializers.LoadEnvVariablesPath(baseDir) | ||||||
|  | 	config.LoadConfig() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // SetDatabase updates the database used by TestEngine
 | ||||||
|  | func SetDatabase(database, connStr, dbSchema string) { | ||||||
|  | 	setDbType = database | ||||||
|  | 	setConnStr = connStr | ||||||
|  | 	setDbSchema = dbSchema | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CleanDatabase removes all test data from the database
 | ||||||
|  | func CleanDatabase() { | ||||||
|  | 	if setDbType == "postgres" { | ||||||
|  | 		ctx := context.Background() | ||||||
|  | 		// Update the connection string with the new search path
 | ||||||
|  | 		updatedConnStr, err := updateSearchPath(setConnStr, setDbSchema) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatalf("Failed to update search path: %v", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		dbConn, err := pgxpool.New(ctx, updatedConnStr) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatalf("Failed to connect to database for cleanup: %v", err) | ||||||
|  | 		} | ||||||
|  | 		defer dbConn.Close() | ||||||
|  | 
 | ||||||
|  | 		query := fmt.Sprintf("DELETE FROM %s.kv_vise;", setDbSchema) | ||||||
|  | 		_, execErr := dbConn.Exec(ctx, query) | ||||||
|  | 		if execErr != nil { | ||||||
|  | 			log.Printf("Failed to cleanup table %s.kv_vise: %v", setDbSchema, execErr) | ||||||
|  | 		} else { | ||||||
|  | 			log.Printf("Successfully cleaned up table %s.kv_vise", setDbSchema) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		setConnStr, _ := filepath.Abs(setConnStr) | ||||||
|  | 		if err := os.RemoveAll(setConnStr); err != nil { | ||||||
|  | 			log.Fatalf("Failed to delete state store %s: %v", setConnStr, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // updateSearchPath updates the search_path (schema) to be used in the connection
 | ||||||
|  | func updateSearchPath(connStr string, newSearchPath string) (string, error) { | ||||||
|  | 	u, err := url.Parse(connStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("invalid connection string: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Parse the query parameters
 | ||||||
|  | 	q := u.Query() | ||||||
|  | 
 | ||||||
|  | 	// Update or add the search_path parameter
 | ||||||
|  | 	q.Set("search_path", newSearchPath) | ||||||
|  | 
 | ||||||
|  | 	// Rebuild the connection string with updated parameters
 | ||||||
|  | 	u.RawQuery = q.Encode() | ||||||
|  | 
 | ||||||
|  | 	return u.String(), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { | ||||||
|  | 	var err error | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||||
|  | 	pfp := path.Join(scriptDir, "pp.csv") | ||||||
|  | 
 | ||||||
|  | 	var eventChannel = make(chan bool) | ||||||
|  | 
 | ||||||
|  | 	cfg := engine.Config{ | ||||||
|  | 		Root:       "root", | ||||||
|  | 		SessionId:  sessionId, | ||||||
|  | 		OutputSize: uint32(160), | ||||||
|  | 		FlagCount:  uint32(128), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if setDbType == "postgres" { | ||||||
|  | 		setConnStr = config.DbConn | ||||||
|  | 		setConnStr, err = updateSearchPath(setConnStr, setDbSchema) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Println("Error:", err) | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		setConnStr, err = filepath.Abs(setConnStr) | ||||||
|  | 		if err != nil { | ||||||
|  | 			fmt.Fprintf(os.Stderr, "connstr err: %v", err) | ||||||
|  | 			os.Exit(1) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	conn, err := storage.ToConnData(setConnStr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "connstr parse err: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	resourceDir := scriptDir | ||||||
|  | 	menuStorageService := storage.NewMenuStorageService(conn, resourceDir) | ||||||
|  | 
 | ||||||
|  | 	rs, err := menuStorageService.GetResource(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "resource error: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pe, err := menuStorageService.GetPersister(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "persister error: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	userDataStore, err := menuStorageService.GetUserdataDb(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "userdb error: %v", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dbResource, ok := rs.(*resource.DbResource) | ||||||
|  | 	if !ok { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "dbresource cast error") | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||||
|  | 	lhs.SetDataStore(&userDataStore) | ||||||
|  | 	lhs.SetPersister(pe) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if testtag.AccountService == nil { | ||||||
|  | 		testtag.AccountService = &remote.AccountService{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch testtag.AccountService.(type) { | ||||||
|  | 	case *testservice.TestAccountService: | ||||||
|  | 		go func() { | ||||||
|  | 			eventChannel <- false | ||||||
|  | 		}() | ||||||
|  | 	case *remote.AccountService: | ||||||
|  | 		go func() { | ||||||
|  | 			time.Sleep(5 * time.Second) // Wait for 5 seconds
 | ||||||
|  | 			eventChannel <- true | ||||||
|  | 		}() | ||||||
|  | 	default: | ||||||
|  | 		panic("Unknown account service type") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	hl, err := lhs.GetHandler(testtag.AccountService) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Fprintf(os.Stderr, err.Error()) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	en := lhs.GetEngine() | ||||||
|  | 	en = en.WithFirst(hl.Init) | ||||||
|  | 	cleanFn := func() { | ||||||
|  | 		err := en.Finish() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logg.Errorf(err.Error()) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		err = menuStorageService.Close() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logg.Errorf(err.Error()) | ||||||
|  | 		} | ||||||
|  | 		logg.Infof("testengine storage closed") | ||||||
|  | 	} | ||||||
|  | 	return en, cleanFn, eventChannel | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								internal/testutil/engine_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								internal/testutil/engine_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | package testutil | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestCreateEngine(t *testing.T) { | ||||||
|  | 	o, clean, eventC := TestEngine("foo") | ||||||
|  | 	defer clean() | ||||||
|  | 	defer func() { | ||||||
|  | 		<-eventC | ||||||
|  | 		close(eventC) | ||||||
|  | 	}() | ||||||
|  | 	_ = o | ||||||
|  | } | ||||||
| @ -1,12 +1,14 @@ | |||||||
| package httpmocks | package httpmocks | ||||||
| 
 | 
 | ||||||
|  | import "context" | ||||||
|  | 
 | ||||||
| // MockRequestParser implements the handlers.RequestParser interface for testing
 | // MockRequestParser implements the handlers.RequestParser interface for testing
 | ||||||
| type MockRequestParser struct { | type MockRequestParser struct { | ||||||
| 	GetSessionIdFunc func(any) (string, error) | 	GetSessionIdFunc func(any) (string, error) | ||||||
| 	GetInputFunc     func(any) ([]byte, error) | 	GetInputFunc     func(any) ([]byte, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *MockRequestParser) GetSessionId(rq any) (string, error) { | func (m *MockRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) { | ||||||
| 	return m.GetSessionIdFunc(rq) | 	return m.GetSessionIdFunc(rq) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -47,3 +47,8 @@ func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to | |||||||
| 	args := m.Called() | 	args := m.Called() | ||||||
| 	return args.Get(0).(*models.TokenTransferResponse), args.Error(1) | 	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) | ||||||
|  | } | ||||||
| @ -33,8 +33,8 @@ func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { | func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { | ||||||
| 	return []dataserviceapi.TokenHoldings { | 	return []dataserviceapi.TokenHoldings{ | ||||||
| 		dataserviceapi.TokenHoldings { | 		dataserviceapi.TokenHoldings{ | ||||||
| 			ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", | 			ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", | ||||||
| 			TokenSymbol:     "SRF", | 			TokenSymbol:     "SRF", | ||||||
| 			TokenDecimals:   "6", | 			TokenDecimals:   "6", | ||||||
| @ -56,3 +56,7 @@ func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, | |||||||
| 		TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af", | 		TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af", | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { | ||||||
|  | 	return &dataserviceapi.AliasAddress{}, nil | ||||||
|  | } | ||||||
| @ -2,8 +2,8 @@ | |||||||
| 
 | 
 | ||||||
| package testtag | package testtag | ||||||
| 
 | 
 | ||||||
| import "git.grassecon.net/urdt/ussd/internal/handlers/server" | import "git.grassecon.net/urdt/ussd/remote" | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	AccountService server.AccountServiceInterface | 	AccountService remote.AccountServiceInterface | ||||||
| ) | ) | ||||||
|  | |||||||
| @ -1,6 +1,9 @@ | |||||||
| package utils | package utils | ||||||
| 
 | 
 | ||||||
| import "time" | import ( | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| // CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
 | // CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
 | ||||||
| // It adjusts for cases where the current date is before the birthday in the current year.
 | // It adjusts for cases where the current date is before the birthday in the current year.
 | ||||||
| @ -25,11 +28,29 @@ func CalculateAge(birthdate, today time.Time) int { | |||||||
| // It subtracts the YOB from the current year to determine the age.
 | // It subtracts the YOB from the current year to determine the age.
 | ||||||
| //
 | //
 | ||||||
| // Parameters:
 | // Parameters:
 | ||||||
|  | //
 | ||||||
| //	yob: The year of birth as an integer.
 | //	yob: The year of birth as an integer.
 | ||||||
| //
 | //
 | ||||||
| // Returns:
 | // Returns:
 | ||||||
|  | //
 | ||||||
| //	The calculated age as an integer.
 | //	The calculated age as an integer.
 | ||||||
| func CalculateAgeWithYOB(yob int) int { | func CalculateAgeWithYOB(yob int) int { | ||||||
| 	currentYear := time.Now().Year() | 	currentYear := time.Now().Year() | ||||||
| 	return currentYear - yob | 	return currentYear - yob | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | //IsValidYob checks if the provided yob can be considered valid 
 | ||||||
|  | func IsValidYOb(yob string) bool { | ||||||
|  | 	currentYear := time.Now().Year() | ||||||
|  | 	yearOfBirth, err := strconv.ParseInt(yob, 10, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	if yearOfBirth >= 1900 && int(yearOfBirth) <= currentYear { | ||||||
|  | 		return true | ||||||
|  | 	} else { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ package utils | |||||||
| var isoCodes = map[string]bool{ | var isoCodes = map[string]bool{ | ||||||
| 	"eng":     true, // English
 | 	"eng":     true, // English
 | ||||||
| 	"swa":     true, // Swahili
 | 	"swa":     true, // Swahili
 | ||||||
| 
 | 	"default": true, // Default language: English
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func IsValidISO639(code string) bool { | func IsValidISO639(code string) bool { | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								internal/utils/name.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								internal/utils/name.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | 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 | ||||||
|  | } | ||||||
| @ -54,7 +54,7 @@ | |||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "1235", |                     "input": "1235", | ||||||
|                     "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" |                     "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "1", |                     "input": "1", | ||||||
| @ -62,10 +62,10 @@ | |||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "1234", |                     "input": "1234", | ||||||
|                     "expectedContent": "Select language:\n0:english\n1:kiswahili" |                     "expectedContent": "Select language:\n1:English\n2:Kiswahili" | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "1", | ||||||
|                     "expectedContent": "Your language change request was successful.\n0:Back\n9:Quit" |                     "expectedContent": "Your language change request was successful.\n0:Back\n9:Quit" | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
| @ -95,7 +95,7 @@ | |||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "1235", |                     "input": "1235", | ||||||
|                     "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" |                     "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "1", |                     "input": "1", | ||||||
| @ -108,7 +108,6 @@ | |||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
|                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" |                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" | ||||||
|                      |  | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
| @ -141,7 +140,7 @@ | |||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "1235", |                     "input": "1235", | ||||||
|                     "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" |                     "expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit" | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "1", |                     "input": "1", | ||||||
| @ -154,7 +153,6 @@ | |||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
|                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" |                     "expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back" | ||||||
|                      |  | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
| @ -167,7 +165,7 @@ | |||||||
|             ] |             ] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "menu_my_account_edit_firstname", |             "name": "menu_my_account_edit_all_account_details_starting_from_firstname", | ||||||
|             "steps": [ |             "steps": [ | ||||||
|                 { |                 { | ||||||
|                     "input": "", |                     "input": "", | ||||||
| @ -187,6 +185,26 @@ | |||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "foo", |                     "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:" |                     "expectedContent": "Please enter your PIN:" | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
| @ -197,10 +215,6 @@ | |||||||
|                     "input": "0", |                     "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" |                     "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": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||||
| @ -208,7 +222,7 @@ | |||||||
|             ] |             ] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "menu_my_account_edit_familyname", |             "name": "menu_my_account_edit_familyname_when_all_account__details_have_been_set", | ||||||
|             "steps": [ |             "steps": [ | ||||||
|                 { |                 { | ||||||
|                     "input": "", |                     "input": "", | ||||||
| @ -238,19 +252,14 @@ | |||||||
|                     "input": "0", |                     "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" |                     "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": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||||
|                 } |                 } | ||||||
|                 |  | ||||||
|             ] |             ] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "menu_my_account_edit_gender", |             "name": "menu_my_account_edit_gender_when_all_account__details_have_been_set", | ||||||
|             "steps": [ |             "steps": [ | ||||||
|                 { |                 { | ||||||
|                     "input": "", |                     "input": "", | ||||||
| @ -280,10 +289,6 @@ | |||||||
|                     "input": "0", |                     "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" |                     "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": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||||
| @ -291,7 +296,7 @@ | |||||||
|             ] |             ] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "menu_my_account_edit_yob", |             "name": "menu_my_account_edit_yob_when_all_account__details_have_been_set", | ||||||
|             "steps": [ |             "steps": [ | ||||||
|                 { |                 { | ||||||
|                     "input": "", |                     "input": "", | ||||||
| @ -321,10 +326,6 @@ | |||||||
|                     "input": "0", |                     "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" |                     "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": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||||
| @ -332,7 +333,7 @@ | |||||||
|             ] |             ] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "menu_my_account_edit_location", |             "name": "menu_my_account_edit_location_when_all_account_details_have_been_set", | ||||||
|             "steps": [ |             "steps": [ | ||||||
|                 { |                 { | ||||||
|                     "input": "", |                     "input": "", | ||||||
| @ -362,10 +363,6 @@ | |||||||
|                     "input": "0", |                     "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" |                     "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": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||||
| @ -373,7 +370,7 @@ | |||||||
|             ] |             ] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "name": "menu_my_account_edit_offerings", |             "name": "menu_my_account_edit_offerings_when_all_account__details_have_been_set", | ||||||
|             "steps": [ |             "steps": [ | ||||||
|                 { |                 { | ||||||
|                     "input": "", |                     "input": "", | ||||||
| @ -403,10 +400,6 @@ | |||||||
|                     "input": "0", |                     "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" |                     "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": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||||
| @ -434,16 +427,12 @@ | |||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "1234", |                     "input": "1234", | ||||||
|                     "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back" |                     "expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit" | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "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" |                     "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": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back" |  | ||||||
|                 }, |  | ||||||
|                 { |                 { | ||||||
|                     "input": "0", |                     "input": "0", | ||||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" |                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||||
| @ -452,9 +441,3 @@ | |||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
| } | } | ||||||
|        |  | ||||||
|          |  | ||||||
|         |  | ||||||
|          |  | ||||||
|      |  | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -3,9 +3,9 @@ package menutraversaltest | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"flag" | ||||||
| 	"log" | 	"log" | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
| 	"os" |  | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| @ -16,13 +16,16 @@ import ( | |||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	testData  = driver.ReadData() | 	testData  = driver.ReadData() | ||||||
| 	testStore     = ".test_state" |  | ||||||
| 	groupTestFile = "group_test.json" |  | ||||||
| 	sessionID string | 	sessionID string | ||||||
| 	src       = rand.NewSource(42) | 	src       = rand.NewSource(42) | ||||||
| 	g         = rand.New(src) | 	g         = rand.New(src) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests") | ||||||
|  | var database = flag.String("db", "gdbm", "Specify the database (gdbm or postgres)") | ||||||
|  | var connStr = flag.String("conn", ".test_state", "connection string") | ||||||
|  | var dbSchema = flag.String("schema", "test", "Specify the database schema (default test)") | ||||||
|  | 
 | ||||||
| func GenerateSessionId() string { | func GenerateSessionId() string { | ||||||
| 	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) | 	uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g)) | ||||||
| 	v, err := uu.NewV4() | 	v, err := uu.NewV4() | ||||||
| @ -77,12 +80,15 @@ func extractSendAmount(response []byte) string { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestMain(m *testing.M) { | func TestMain(m *testing.M) { | ||||||
|  | 	// Parse the flags
 | ||||||
|  | 	flag.Parse() | ||||||
| 	sessionID = GenerateSessionId() | 	sessionID = GenerateSessionId() | ||||||
| 	defer func() { | 	// set the db
 | ||||||
| 		if err := os.RemoveAll(testStore); err != nil { | 	testutil.SetDatabase(*database, *connStr, *dbSchema) | ||||||
| 			log.Fatalf("Failed to delete state store %s: %v", testStore, err) | 
 | ||||||
| 		} | 	// Cleanup the db after tests
 | ||||||
| 	}() | 	defer testutil.CleanDatabase() | ||||||
|  | 
 | ||||||
| 	m.Run() | 	m.Run() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -119,7 +125,6 @@ func TestAccountCreationSuccessful(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	<-eventChannel | 	<-eventChannel | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestAccountRegistrationRejectTerms(t *testing.T) { | func TestAccountRegistrationRejectTerms(t *testing.T) { | ||||||
| @ -296,9 +301,10 @@ func TestMainMenuSend(t *testing.T) { | |||||||
| 	ctx := context.Background() | 	ctx := context.Background() | ||||||
| 	sessions := testData | 	sessions := testData | ||||||
| 	for _, session := range sessions { | 	for _, session := range sessions { | ||||||
| 		groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs") | 		groups := driver.FilterGroupsByName(session.Groups, "send_with_invite") | ||||||
| 		for _, group := range groups { | 		for _, group := range groups { | ||||||
| 			for _, step := range group.Steps { | 			for index, step := range group.Steps { | ||||||
|  | 				t.Logf("step %v with input %v", index, step.Input) | ||||||
| 				cont, err := en.Exec(ctx, []byte(step.Input)) | 				cont, err := en.Exec(ctx, []byte(step.Input)) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) | 					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) | ||||||
| @ -337,7 +343,7 @@ func TestMainMenuSend(t *testing.T) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestGroups(t *testing.T) { | func TestGroups(t *testing.T) { | ||||||
| 	groups, err := driver.LoadTestGroups(groupTestFile) | 	groups, err := driver.LoadTestGroups(*groupTestFile) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Failed to load test groups: %v", err) | 		log.Fatalf("Failed to load test groups: %v", err) | ||||||
| 	} | 	} | ||||||
|  | |||||||
							
								
								
									
										68
									
								
								menutraversal_test/profile_edit_start_familyname.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								menutraversal_test/profile_edit_start_familyname.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | { | ||||||
|  |     "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" | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |        | ||||||
|  |          | ||||||
|  |         | ||||||
|  |          | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |      | ||||||
							
								
								
									
										61
									
								
								menutraversal_test/profile_edit_start_firstname.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								menutraversal_test/profile_edit_start_firstname.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | |||||||
|  | { | ||||||
|  |     "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" | ||||||
|  |                 } | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								menutraversal_test/profile_edit_start_gender.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								menutraversal_test/profile_edit_start_gender.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | |||||||
|  | { | ||||||
|  |     "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" | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |        | ||||||
|  |        | ||||||
							
								
								
									
										46
									
								
								menutraversal_test/profile_edit_start_location.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								menutraversal_test/profile_edit_start_location.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | { | ||||||
|  |     "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" | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |       | ||||||
							
								
								
									
										42
									
								
								menutraversal_test/profile_edit_start_offerings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								menutraversal_test/profile_edit_start_offerings.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | { | ||||||
|  |     "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" | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |       | ||||||
							
								
								
									
										50
									
								
								menutraversal_test/profile_edit_start_yob.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								menutraversal_test/profile_edit_start_yob.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | |||||||
|  | { | ||||||
|  |     "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" | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |       | ||||||
							
								
								
									
										70
									
								
								menutraversal_test/profile_edit_when_adjacent_item_set.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								menutraversal_test/profile_edit_when_adjacent_item_set.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | |||||||
|  | { | ||||||
|  |     "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" | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             } | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |      | ||||||
| @ -7,14 +7,14 @@ | |||||||
|                 "steps": [ |                 "steps": [ | ||||||
|                     { |                     { | ||||||
|                         "input": "", |                         "input": "", | ||||||
|                         "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili" |                         "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili" | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         "input": "0", |                         "input": "1", | ||||||
|                         "expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no" |                         "expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No" | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         "input": "0", |                         "input": "1", | ||||||
|                         "expectedContent": "Please enter a new four number PIN for your account:\n0:Exit" |                         "expectedContent": "Please enter a new four number PIN for your account:\n0:Exit" | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
| @ -40,14 +40,14 @@ | |||||||
|                 "steps": [ |                 "steps": [ | ||||||
|                     { |                     { | ||||||
|                         "input": "", |                         "input": "", | ||||||
|                         "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili" |                         "expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili" | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         "input": "0", |  | ||||||
|                         "expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no" |  | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         "input": "1", |                         "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!" |                         "expectedContent": "Thank you for using Sarafu. Goodbye!" | ||||||
|                     } |                     } | ||||||
|                 ] |                 ] | ||||||
| @ -61,15 +61,15 @@ | |||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         "input": "1", |                         "input": "1", | ||||||
|                         "expectedContent": "Enter recipient's phone number:\n0:Back" |                         "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         "input": "000", |                         "input": "0@0", | ||||||
|                         "expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit" |                         "expectedContent": "0@0 is invalid, please try again:\n1:Retry\n9:Quit" | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         "input": "1", |                         "input": "1", | ||||||
|                         "expectedContent": "Enter recipient's phone number:\n0:Back" |                         "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         "input": "0712345678", |                         "input": "0712345678", | ||||||
|  | |||||||
							
								
								
									
										18
									
								
								models/profile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								models/profile.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | 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 | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -5,4 +5,6 @@ type VoucherDataResult struct { | |||||||
| 	TokenSymbol    string `json:"tokenSymbol"` | 	TokenSymbol    string `json:"tokenSymbol"` | ||||||
| 	TokenDecimals  int    `json:"tokenDecimals"` | 	TokenDecimals  int    `json:"tokenDecimals"` | ||||||
| 	SinkAddress    string `json:"sinkAddress"` | 	SinkAddress    string `json:"sinkAddress"` | ||||||
|  | 	TokenCommodity string `json:"tokenCommodity"` | ||||||
|  | 	TokenLocation  string `json:"tokenLocation"` | ||||||
| } | } | ||||||
|  | |||||||
| @ -24,6 +24,7 @@ type AccountServiceInterface interface { | |||||||
| 	FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) | 	FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) | ||||||
| 	VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) | 	VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) | ||||||
| 	TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, 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 { | type AccountService struct { | ||||||
| @ -209,6 +210,26 @@ func (as *AccountService) TokenTransfer(ctx context.Context, amount, from, to, t | |||||||
| 	return &r, nil | 	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) { | func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { | ||||||
| 	var okResponse api.OKResponse | 	var okResponse api.OKResponse | ||||||
| 	var errResponse api.ErrResponse | 	var errResponse api.ErrResponse | ||||||
							
								
								
									
										1
									
								
								services/registration/_catch_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/_catch_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye. | ||||||
| @ -7,3 +7,4 @@ HALT | |||||||
| INCMP _ 0 | INCMP _ 0 | ||||||
| INCMP my_balance 1 | INCMP my_balance 1 | ||||||
| INCMP community_balance 2 | INCMP community_balance 2 | ||||||
|  | INCMP . *  | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								services/registration/blocked_account.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/blocked_account.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | LOAD show_blocked_account 0 | ||||||
|  | HALT  | ||||||
| @ -2,9 +2,9 @@ LOAD reset_account_authorized 0 | |||||||
| LOAD reset_incorrect 0 | LOAD reset_incorrect 0 | ||||||
| CATCH incorrect_pin flag_incorrect_pin 1 | CATCH incorrect_pin flag_incorrect_pin 1 | ||||||
| CATCH pin_entry flag_account_authorized 0 | CATCH pin_entry flag_account_authorized 0 | ||||||
| MOUT english 0 | MOUT english 1 | ||||||
| MOUT kiswahili 1 | MOUT kiswahili 2 | ||||||
| HALT | HALT | ||||||
| INCMP set_default 0 | INCMP set_eng 1 | ||||||
| INCMP set_swa 1 | INCMP set_swa 2 | ||||||
| INCMP . * | INCMP . * | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								services/registration/check_statement
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/check_statement
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | Please enter your PIN to view statement: | ||||||
							
								
								
									
										12
									
								
								services/registration/check_statement.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								services/registration/check_statement.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | LOAD check_transactions 0 | ||||||
|  | RELOAD check_transactions | ||||||
|  | CATCH no_transfers flag_no_transfers 1 | ||||||
|  | LOAD authorize_account 6 | ||||||
|  | MOUT back 0 | ||||||
|  | MOUT quit 9 | ||||||
|  | HALT | ||||||
|  | RELOAD authorize_account | ||||||
|  | CATCH incorrect_pin flag_incorrect_pin 1 | ||||||
|  | INCMP _ 0 | ||||||
|  | INCMP quit 9 | ||||||
|  | INCMP transactions * | ||||||
							
								
								
									
										1
									
								
								services/registration/check_statement_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/check_statement_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | Tafadhali weka PIN yako kuona taarifa ya matumizi: | ||||||
| @ -9,3 +9,4 @@ MOUT quit 9 | |||||||
| HALT | HALT | ||||||
| INCMP _ 0 | INCMP _ 0 | ||||||
| INCMP quit 9 | INCMP quit 9 | ||||||
|  | INCMP . *  | ||||||
|  | |||||||
| @ -2,9 +2,17 @@ CATCH incorrect_pin flag_incorrect_pin 1 | |||||||
| CATCH update_familyname flag_allow_update 1 | CATCH update_familyname flag_allow_update 1 | ||||||
| LOAD get_current_profile_info 0 | LOAD get_current_profile_info 0 | ||||||
| RELOAD get_current_profile_info | RELOAD get_current_profile_info | ||||||
|  | MAP get_current_profile_info | ||||||
| MOUT back 0 | MOUT back 0 | ||||||
| HALT | HALT | ||||||
| LOAD save_familyname 0 | RELOAD set_back  | ||||||
|  | CATCH _ flag_back_set 1 | ||||||
|  | LOAD save_familyname 64 | ||||||
| RELOAD save_familyname | RELOAD save_familyname | ||||||
| INCMP _ 0 | CATCH pin_entry flag_familyname_set 1 | ||||||
| INCMP pin_entry * | CATCH select_gender flag_gender_set 0 | ||||||
|  | CATCH edit_yob flag_yob_set 0 | ||||||
|  | CATCH edit_location flag_location_set 0 | ||||||
|  | CATCH edit_offerings flag_offerings_set 0 | ||||||
|  | CATCH pin_entry flag_familyname_set 0 | ||||||
|  | INCMP select_gender * | ||||||
|  | |||||||
| @ -5,7 +5,14 @@ RELOAD get_current_profile_info | |||||||
| MAP get_current_profile_info | MAP get_current_profile_info | ||||||
| MOUT back 0 | MOUT back 0 | ||||||
| HALT | HALT | ||||||
| LOAD save_firstname 0 | RELOAD set_back  | ||||||
|  | CATCH _ flag_back_set 1 | ||||||
|  | LOAD save_firstname 128 | ||||||
| RELOAD save_firstname | RELOAD save_firstname | ||||||
| INCMP _ 0 | CATCH pin_entry flag_firstname_set 1 | ||||||
| INCMP pin_entry * | CATCH edit_family_name flag_familyname_set 0 | ||||||
|  | CATCH edit_gender flag_gender_set 0 | ||||||
|  | CATCH edit_yob flag_yob_set 0 | ||||||
|  | CATCH edit_location flag_location_set 0 | ||||||
|  | CATCH edit_offerings flag_offerings_set 0 | ||||||
|  | CATCH pin_entry flag_firstname_set 0 | ||||||
|  | |||||||
| @ -1,2 +1,2 @@ | |||||||
| Jina la kwanza la sasa {{.get_current_profile_info}} | Jina la kwanza la sasa: {{.get_current_profile_info}} | ||||||
| Weka majina yako ya kwanza: | Weka majina yako ya kwanza: | ||||||
| @ -2,9 +2,14 @@ CATCH incorrect_pin flag_incorrect_pin 1 | |||||||
| CATCH update_location flag_allow_update 1 | CATCH update_location flag_allow_update 1 | ||||||
| LOAD get_current_profile_info 0 | LOAD get_current_profile_info 0 | ||||||
| RELOAD get_current_profile_info | RELOAD get_current_profile_info | ||||||
|  | LOAD save_location 16 | ||||||
| MOUT back 0 | MOUT back 0 | ||||||
| HALT | HALT | ||||||
| LOAD save_location 0 | RELOAD set_back  | ||||||
|  | CATCH _ flag_back_set 1 | ||||||
| RELOAD save_location | RELOAD save_location | ||||||
| INCMP _ 0 | INCMP _ 0 | ||||||
| INCMP pin_entry * | CATCH pin_entry flag_location_set 1 | ||||||
|  | CATCH edit_offerings flag_offerings_set 0 | ||||||
|  | CATCH pin_entry flag_location_set 0 | ||||||
|  | INCMP edit_offerings * | ||||||
|  | |||||||
| @ -1,2 +1,2 @@ | |||||||
| Eneo la sasa {{.get_current_profile_info}} | Eneo la sasa: {{.get_current_profile_info}} | ||||||
| Weka eneo: | Weka eneo: | ||||||
| @ -2,9 +2,12 @@ CATCH incorrect_pin flag_incorrect_pin 1 | |||||||
| CATCH update_offerings flag_allow_update 1 | CATCH update_offerings flag_allow_update 1 | ||||||
| LOAD get_current_profile_info 0 | LOAD get_current_profile_info 0 | ||||||
| RELOAD get_current_profile_info | RELOAD get_current_profile_info | ||||||
| LOAD save_offerings 0 | LOAD save_offerings 8 | ||||||
| MOUT back 0 | MOUT back 0 | ||||||
| HALT | HALT | ||||||
|  | RELOAD set_back  | ||||||
|  | CATCH _ flag_back_set 1 | ||||||
| RELOAD save_offerings | RELOAD save_offerings | ||||||
| INCMP _ 0 | INCMP _ 0 | ||||||
| INCMP pin_entry * | CATCH pin_entry flag_offerings_set 1 | ||||||
|  | INCMP update_profile_items * | ||||||
|  | |||||||
| @ -11,7 +11,8 @@ MOUT edit_offerings 6 | |||||||
| MOUT view 7 | MOUT view 7 | ||||||
| MOUT back 0 | MOUT back 0 | ||||||
| HALT | HALT | ||||||
| INCMP my_account 0 | LOAD set_back 6 | ||||||
|  | INCMP ^ 0 | ||||||
| INCMP edit_first_name 1 | INCMP edit_first_name 1 | ||||||
| INCMP edit_family_name 2 | INCMP edit_family_name 2 | ||||||
| INCMP select_gender 3 | INCMP select_gender 3 | ||||||
| @ -19,3 +20,4 @@ INCMP edit_yob 4 | |||||||
| INCMP edit_location 5 | INCMP edit_location 5 | ||||||
| INCMP edit_offerings 6 | INCMP edit_offerings 6 | ||||||
| INCMP view_profile 7 | INCMP view_profile 7 | ||||||
|  | INCMP . *  | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user