diff options
| author | xengineering <me@xengineering.eu> | 2026-06-21 11:49:42 +0200 |
|---|---|---|
| committer | xengineering <me@xengineering.eu> | 2026-06-21 11:55:44 +0200 |
| commit | 7ab03af3f662c90b1868f5ef14e70f443b2d91cd (patch) | |
| tree | b82e9fc9b102bec32aa9f0705b52c446df8da60b | |
| parent | 383cf7a521f7bfcdf552cd18ea9919930af2e9cf (diff) | |
| download | finserv-7ab03af3f662c90b1868f5ef14e70f443b2d91cd.tar finserv-7ab03af3f662c90b1868f5ef14e70f443b2d91cd.tar.zst finserv-7ab03af3f662c90b1868f5ef14e70f443b2d91cd.zip | |
Add `register` HTTP handler
This allows users to register.
| -rw-r--r-- | handlers.go | 51 | ||||
| -rw-r--r-- | handlers_test.go | 82 | ||||
| -rw-r--r-- | main.go | 5 | ||||
| -rw-r--r-- | password_test.go | 2 |
4 files changed, 119 insertions, 21 deletions
diff --git a/handlers.go b/handlers.go index d28a491..e6d60fb 100644 --- a/handlers.go +++ b/handlers.go @@ -1,7 +1,9 @@ package main import ( + "database/sql" "embed" + "encoding/json" "fmt" "io/fs" "log" @@ -13,8 +15,9 @@ import ( //go:embed build/frontend/public var frontendEmbed embed.FS -func registerHandlers(r *mux.Router) { - r.HandleFunc("/api/version", version) +func registerHandlers(r *mux.Router, db *sql.DB) { + r.Handle("/api/version", version(db)) + r.Handle("/api/registration", register(db)) // frontend must come last to make sure /api takes precedence frontend, err := fs.Sub(frontendEmbed, "build/frontend/public") @@ -24,6 +27,46 @@ func registerHandlers(r *mux.Router) { r.PathPrefix("/").Handler(http.FileServerFS(frontend)) } -func version(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, versionTxt) +func version(db *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, versionTxt) + }) +} + +func register(db *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var data struct { + Username string + Password string + } + + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + err := dec.Decode(&data) + if err != nil { + log.Printf("Failed to decode: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + if data.Username == "" { + log.Print("Got registration request with empty username") + w.WriteHeader(http.StatusBadRequest) + return + } + + if data.Password == "" { + log.Print("Got registration request with empty password") + w.WriteHeader(http.StatusBadRequest) + return + } + + err = Register(db, data.Username, data.Password) + if err != nil { + log.Printf("Failed to register new user: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + }) } diff --git a/handlers_test.go b/handlers_test.go index 4e28de7..3e226d0 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -1,30 +1,86 @@ package main import ( + "bytes" + "database/sql" "net/http" "net/http/httptest" "testing" ) +type Handler func(db *sql.DB) http.Handler + +type httpCase struct { + name string + method string + input []byte + output []byte + status int +} + +func testHandler( + t *testing.T, + handler Handler, + cases []httpCase, + setup func(t *testing.T) *sql.DB, +) { + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + db := setup(t) + + in := bytes.NewReader(c.input) + + rr := httptest.NewRecorder() + + req, err := http.NewRequest("POST", "/", in) + if err != nil { + t.Fatal(err) + } + + handler(db).ServeHTTP(rr, req) + + if rr.Code != c.status { + t.Errorf("handler returned wrong status code: got %v want %v", + rr.Code, c.status) + } + + if rr.Body.String() != string(c.output) { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), string(c.output)) + } + }) + } +} + func TestHTTPVersion(t *testing.T) { - req, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) + cases := []httpCase{ + {"basic", "GET", []byte(``), []byte(versionTxt), http.StatusOK}, } - rr := httptest.NewRecorder() - handler := http.HandlerFunc(version) + setup := func(t *testing.T) *sql.DB { + return nil + } - handler.ServeHTTP(rr, req) + testHandler(t, version, cases, setup) +} - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) +func TestHTTPRegister(t *testing.T) { + cases := []httpCase{ + {"basic", "POST", []byte(`{"username":"testuser","password":"mypass"}`), []byte(""), http.StatusOK}, + {"missing-password", "POST", []byte(`{"username":"testuser"}`), []byte(""), http.StatusBadRequest}, + {"missing-username", "POST", []byte(`{"password":"mypass"}`), []byte(""), http.StatusBadRequest}, + {"unknown-field", "POST", []byte(`{"username":"testuser","password":"mypass","foo":"bar"}`), []byte(""), http.StatusBadRequest}, + {"invalid-json", "POST", []byte(`{username":"testuser","password":"mypass"}`), []byte(""), http.StatusBadRequest}, } - expected := versionTxt - if rr.Body.String() != expected { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) + setup := func(t *testing.T) *sql.DB { + db, _ := emptyDB(t) + err := MigrateToLatest(db) + if err != nil { + t.Fatalf("Could not migrate test database: %v", err) + } + return db } + + testHandler(t, register, cases, setup) } @@ -24,7 +24,6 @@ const ( //go:embed VERSION.txt var versionTxt string var router *mux.Router = mux.NewRouter() -var db *sql.DB func init() { log.SetFlags(0) @@ -53,7 +52,7 @@ func run( defer log.Println("finserv finance server was stopped.") var err error - db, err = sql.Open("sqlite3", dbPath) + db, err := sql.Open("sqlite3", dbPath) if err != nil { log.Fatalf("Failed to open SQLite3 database '%s': %v", dbPath, err) } @@ -64,7 +63,7 @@ func run( log.Fatalf("Failed to migrate SQLite3 database '%s': %v", dbPath, err) } - registerHandlers(router) + registerHandlers(router, db) server := &http.Server{ Handler: handler, diff --git a/password_test.go b/password_test.go index 9d89b8f..a577194 100644 --- a/password_test.go +++ b/password_test.go @@ -3,8 +3,8 @@ package main import ( "testing" - "golang.org/x/crypto/bcrypt" _ "github.com/mattn/go-sqlite3" + "golang.org/x/crypto/bcrypt" ) var passwords = []struct { |
