From fc6fb9914ec96d13420a787c83c604a3fcdd71a2 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Wed, 4 Oct 2023 15:37:04 +0200 Subject: [PATCH] Test login + callback --- Makefile | 2 + db/db.go | 42 +++++++ init.sql => db/init.sql | 0 db/types.go | 2 +- db/user.go | 4 + go.mod | 3 + go.sum | 6 + server/router/handler/login_test.go | 182 ++++++++++++++++++++++++++++ server/router/template.go | 8 +- server/server.go | 2 +- 10 files changed, 243 insertions(+), 8 deletions(-) rename init.sql => db/init.sql (100%) create mode 100644 server/router/handler/login_test.go diff --git a/Makefile b/Makefile index 6c52220..1a65056 100644 --- a/Makefile +++ b/Makefile @@ -10,3 +10,5 @@ delphi.market: $(SOURCE) run: go run . +test: + go test ./server/... ./db/... ./lnd/... diff --git a/db/db.go b/db/db.go index 41b1eef..5f9c13e 100644 --- a/db/db.go +++ b/db/db.go @@ -2,6 +2,8 @@ package db import ( "database/sql" + "fmt" + "io/ioutil" _ "github.com/lib/pq" ) @@ -10,6 +12,10 @@ type DB struct { *sql.DB } +var ( + initSqlPath = "./db/init.sql" +) + func New(dbUrl string) (*DB, error) { var ( db_ *sql.DB @@ -27,3 +33,39 @@ func New(dbUrl string) (*DB, error) { db = &DB{DB: db_} return db, nil } + +func (db *DB) Reset(dbName string) error { + var ( + f []byte + err error + ) + if err = db.Clear(dbName); err != nil { + return err + } + if f, err = ioutil.ReadFile(initSqlPath); err != nil { + return err + } + if _, err = db.Exec(string(f)); err != nil { + return err + } + return nil +} + +func (db *DB) Clear(dbName string) error { + var ( + tables = []string{"lnauth", "users", "sessions", "markets", "shares", "invoices", "order_side", "orders", "matches"} + sql []string + err error + ) + for _, t := range tables { + sql = append(sql, fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE", t)) + } + sql = append(sql, "DROP EXTENSION IF EXISTS \"uuid-ossp\"") + sql = append(sql, "DROP TYPE IF EXISTS order_side") + for _, s := range sql { + if _, err = db.Exec(s); err != nil { + return err + } + } + return nil +} diff --git a/init.sql b/db/init.sql similarity index 100% rename from init.sql rename to db/init.sql diff --git a/db/types.go b/db/types.go index c899698..20a85f4 100644 --- a/db/types.go +++ b/db/types.go @@ -12,7 +12,7 @@ type ( LNAuth struct { K1 string LNURL string - CreatdAt time.Time + CreatedAt time.Time SessionId string } User struct { diff --git a/db/user.go b/db/user.go index f5a2d23..290c651 100644 --- a/db/user.go +++ b/db/user.go @@ -7,6 +7,10 @@ func (db *DB) CreateUser(u *User) error { return err } +func (db *DB) FetchUser(pubkey string, u *User) error { + return db.QueryRow("SELECT pubkey, last_seen FROM users WHERE pubkey = $1", pubkey).Scan(&u.Pubkey, &u.LastSeen) +} + func (db *DB) UpdateUser(u *User) error { _, err := db.Exec("UPDATE users SET last_seen = $1 WHERE pubkey = $2", u.LastSeen, u.Pubkey) return err diff --git a/go.mod b/go.mod index f0ac705..b1c08f9 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/lightningnetwork/lnd v0.10.0-beta.rc6.0.20200615174244-103c59a4889f github.com/namsral/flag v1.7.4-pre github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/stretchr/testify v1.8.1 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 gopkg.in/guregu/null.v4 v4.0.0 ) @@ -72,6 +73,7 @@ require ( github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v0.9.3 // indirect github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect github.com/prometheus/common v0.4.0 // indirect @@ -98,5 +100,6 @@ require ( gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect gopkg.in/macaroon.v2 v2.1.0 // indirect gopkg.in/yaml.v2 v2.2.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index d049902..ae29e4e 100644 --- a/go.sum +++ b/go.sum @@ -253,12 +253,17 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -396,6 +401,7 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/server/router/handler/login_test.go b/server/router/handler/login_test.go new file mode 100644 index 0000000..ac66e6a --- /dev/null +++ b/server/router/handler/login_test.go @@ -0,0 +1,182 @@ +package handler_test + +import ( + "crypto/ecdsa" + "crypto/rand" + "database/sql" + "encoding/hex" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path" + "runtime" + "testing" + + db_ "git.ekzyis.com/ekzyis/delphi.market/db" + "git.ekzyis.com/ekzyis/delphi.market/server/auth" + "git.ekzyis.com/ekzyis/delphi.market/server/router" + "git.ekzyis.com/ekzyis/delphi.market/server/router/context" + "git.ekzyis.com/ekzyis/delphi.market/server/router/handler" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +var ( + dbName string = "delphi_test" + dbUrl string = fmt.Sprintf("postgres://delphi:delphi@localhost:5432/%s?sslmode=disable", dbName) + db *db_.DB +) + +func init() { + // for ParseTemplates to work, cwd needs to be project root + _, filename, _, _ := runtime.Caller(0) + dir := path.Join(path.Dir(filename), "../../..") + err := os.Chdir(dir) + if err != nil { + panic(err) + } + if db, err = db_.New(dbUrl); err != nil { + panic(err) + } +} + +func TestMain(m *testing.M) { + if err := db.Reset(dbName); err != nil { + panic(err) + } + retCode := m.Run() + if err := db.Clear(dbName); err != nil { + panic(err) + } + os.Exit(retCode) +} + +func mocks(method string, target string, body io.Reader) (*echo.Echo, context.ServerContext, *http.Request, *httptest.ResponseRecorder) { + var ( + e *echo.Echo + sc context.ServerContext + req *http.Request + rec *httptest.ResponseRecorder + ) + e = echo.New() + e.Renderer = router.ParseTemplates("pages/**.html") + sc = context.ServerContext{ + Db: db, + } + req = httptest.NewRequest(method, target, body) + rec = httptest.NewRecorder() + return e, sc, req, rec +} + +func TestLogin(t *testing.T) { + var ( + assert = assert.New(t) + e *echo.Echo + c echo.Context + sc context.ServerContext + req *http.Request + rec *httptest.ResponseRecorder + cookies []*http.Cookie + sessionId string + dbSessionId string + err error + ) + e, sc, req, rec = mocks("GET", "/login", nil) + c = e.NewContext(req, rec) + + err = handler.HandleLogin(sc)(c) + assert.NoErrorf(err, "handler returned error") + + // Set-Cookie header present + cookies = rec.Result().Cookies() + assert.Equalf(len(cookies), 1, "wrong number of Set-Cookie headers") + assert.Equalf(cookies[0].Name, "session", "wrong cookie name") + + // new challenge inserted + sessionId = cookies[0].Value + err = db.QueryRow("SELECT session_id FROM lnauth WHERE session_id = $1", sessionId).Scan(&dbSessionId) + if !assert.NoError(err) { + return + } + + // inserted challenge matches cookie value + assert.Equalf(sessionId, dbSessionId, "wrong session id") +} + +func TestLoginCallback(t *testing.T) { + var ( + assert = assert.New(t) + e *echo.Echo + c echo.Context + sc context.ServerContext + req *http.Request + rec *httptest.ResponseRecorder + lnAuth *auth.LNAuth + dbLnAuth *db_.LNAuth + u *db_.User + s *db_.Session + key string + sig string + err error + ) + lnAuth, err = auth.NewLNAuth() + if !assert.NoErrorf(err, "error creating challenge") { + return + } + dbLnAuth = &db_.LNAuth{K1: lnAuth.K1, LNURL: lnAuth.LNURL} + err = db.CreateLNAuth(dbLnAuth) + if !assert.NoErrorf(err, "error inserting challenge") { + return + } + + key, sig, err = sign(lnAuth.K1) + if !assert.NoErrorf(err, "error signing k1") { + return + } + + e, sc, req, rec = mocks("GET", fmt.Sprintf("/api/login?k1=%s&key=%s&sig=%s", lnAuth.K1, key, sig), nil) + c = e.NewContext(req, rec) + + err = handler.HandleLoginCallback(sc)(c) + assert.NoErrorf(err, "handler returned error") + + // user created + u = new(db_.User) + err = db.FetchUser(key, u) + if assert.NoErrorf(err, "error fetching user") { + assert.Equalf(u.Pubkey, key, "pubkeys do not match") + } + + // session created + s = &db_.Session{SessionId: dbLnAuth.SessionId} + err = db.FetchSession(s) + if assert.NoErrorf(err, "error fetching session") { + assert.Equalf(s.Pubkey, u.Pubkey, "session pubkey does not match user pubkey") + } + + // challenge deleted + err = db.FetchSessionId(u.Pubkey, &dbLnAuth.SessionId) + assert.ErrorIs(err, sql.ErrNoRows, "challenge not deleted") +} + +func sign(k1_ string) (string, string, error) { + var ( + sk *secp256k1.PrivateKey + k1 []byte + sig []byte + err error + ) + if k1, err = hex.DecodeString(k1_); err != nil { + return "", "", err + } + if sk, err = secp256k1.GeneratePrivateKey(); err != nil { + return "", "", err + } + if sig, err = ecdsa.SignASN1(rand.Reader, sk.ToECDSA(), k1); err != nil { + return "", "", err + } + return hex.EncodeToString(sk.PubKey().SerializeCompressed()), hex.EncodeToString(sig), nil +} diff --git a/server/router/template.go b/server/router/template.go index d0511c0..242ac1f 100644 --- a/server/router/template.go +++ b/server/router/template.go @@ -20,12 +20,8 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Con return t.templates.ExecuteTemplate(w, name, data) } -var ( - T *Template -) - -func init() { - T = &Template{ +func ParseTemplates(pattern string) *Template { + return &Template{ templates: template.Must(template.New("").Funcs(template.FuncMap{ "add": add[int64], "sub": sub[int64], diff --git a/server/server.go b/server/server.go index d896a84..d104165 100644 --- a/server/server.go +++ b/server/server.go @@ -20,7 +20,7 @@ func New(ctx ServerContext) *Server { ) e = echo.New() e.Static("/", "public") - e.Renderer = router.T + e.Renderer = router.ParseTemplates("pages/**.html") e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "${time_custom} ${method} ${uri} ${status}\n", CustomTimeFormat: "2006-01-02 15:04:05.00000-0700",