Organize backend into product executable and shared Go modules
This commit is contained in:
1 parent
981e84a4b4
commit
f0720ec66b
382 files changed
+1944
-6989
No files matched your search
@@ -0,0 +1,269 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
customermodel "git.sendico.io/tech/sxchnge/product/customers/model"
|
||||
sessionmodel "git.sendico.io/tech/sxchnge/product/sessions/model"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/auth/config"
|
||||
"git.sendico.io/tech/sxchnge/product/auth/request"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"git.sendico.io/tech/sxchnge/shared/service"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestLoginRejectsUnknownClientID(t *testing.T) {
|
||||
testRejectsUnknownClientID(t, "/login", `{
|
||||
"login":{"login":"user@example.com","password":"correct horse battery staple"},
|
||||
"clientId":"missing-client",
|
||||
"deviceId":"device-1"
|
||||
}`, 0, func(router *PublicRouter, req *http.Request) http.HandlerFunc {
|
||||
return router.login(req)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPILoginRejectsUnknownClientID(t *testing.T) {
|
||||
testRejectsUnknownClientID(t, "/login/api", `{
|
||||
"login":{"login":"user@example.com","password":"correct horse battery staple"},
|
||||
"clientId":"missing-client",
|
||||
"deviceId":"device-1",
|
||||
"clientSecret":"secret"
|
||||
}`, 1, func(router *PublicRouter, req *http.Request) http.HandlerFunc {
|
||||
return router.apiLogin(req)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoginAuthenticatesAgainstInjectedAccountStorage(t *testing.T) {
|
||||
account := testActiveAccount(t)
|
||||
unusedDB := &testAccountDB{err: merrors.ErrNoData}
|
||||
activeDB := &testAccountDB{result: account}
|
||||
router := &PublicRouter{
|
||||
logger: zap.NewNop(),
|
||||
db: activeDB,
|
||||
service: mservice.Accounts,
|
||||
}
|
||||
|
||||
got, failed, h := router.authenticateAccount(context.Background(), &srequest.Login{
|
||||
LoginData: customermodel.LoginData{
|
||||
UserDataBase: customermodel.UserDataBase{
|
||||
Login: "user@example.com",
|
||||
},
|
||||
Password: "correct horse battery staple",
|
||||
},
|
||||
})
|
||||
if h != nil {
|
||||
t.Fatal("expected login authentication to succeed against injected account store")
|
||||
}
|
||||
if failed {
|
||||
t.Fatal("successful login was classified as a credential failure")
|
||||
}
|
||||
if got == nil || got.Login != account.Login {
|
||||
t.Fatalf("expected account %q, got %#v", account.Login, got)
|
||||
}
|
||||
if activeDB.getByEmailCalls != 1 {
|
||||
t.Fatalf("expected one active account store lookup, got %d", activeDB.getByEmailCalls)
|
||||
}
|
||||
if unusedDB.getByEmailCalls != 0 {
|
||||
t.Fatalf("expected no unused account store lookups, got %d", unusedDB.getByEmailCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func testRejectsUnknownClientID(
|
||||
t *testing.T,
|
||||
path string,
|
||||
body string,
|
||||
wantLookups int,
|
||||
handler func(*PublicRouter, *http.Request) http.HandlerFunc,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
account := testActiveAccount(t)
|
||||
accountDB := &testAccountDB{result: account}
|
||||
router := &PublicRouter{
|
||||
logger: zap.NewNop(),
|
||||
db: accountDB,
|
||||
rtdb: &testRefreshTokenDB{clientErr: merrors.ErrNoData},
|
||||
service: mservice.Accounts,
|
||||
}
|
||||
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, path, strings.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler(router, req)(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code)
|
||||
}
|
||||
if accountDB.getByEmailCalls != wantLookups {
|
||||
t.Fatalf("expected %d account lookups, got %d", wantLookups, accountDB.getByEmailCalls)
|
||||
}
|
||||
|
||||
resp := decodeErrorResponse(t, rec.Body.Bytes())
|
||||
if resp.Data.Details != "unknown client id" {
|
||||
t.Fatalf("expected unknown client id message, got %q", resp.Data.Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRejectsPendingVerificationAccount(t *testing.T) {
|
||||
account := testAccountWithStatus(t, customermodel.AccountPendingVerification)
|
||||
router := &PublicRouter{
|
||||
logger: zap.NewNop(),
|
||||
db: &testAccountDB{result: account},
|
||||
service: mservice.Accounts,
|
||||
}
|
||||
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/login", strings.NewReader(`{
|
||||
"login":{"login":"user@example.com","password":"correct horse battery staple"}
|
||||
}`))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.login(req)(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusForbidden, rec.Code)
|
||||
}
|
||||
resp := decodeErrorResponse(t, rec.Body.Bytes())
|
||||
if resp.Data.Details != "Account verification required" {
|
||||
t.Fatalf("expected account verification required message, got %q", resp.Data.Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRejectsBlockedAccount(t *testing.T) {
|
||||
account := testAccountWithStatus(t, customermodel.AccountBlocked)
|
||||
router := &PublicRouter{
|
||||
logger: zap.NewNop(),
|
||||
db: &testAccountDB{result: account},
|
||||
service: mservice.Accounts,
|
||||
}
|
||||
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/login", strings.NewReader(`{
|
||||
"login":{"login":"user@example.com","password":"correct horse battery staple"}
|
||||
}`))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.login(req)(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusForbidden, rec.Code)
|
||||
}
|
||||
resp := decodeErrorResponse(t, rec.Body.Bytes())
|
||||
if resp.Data.Details != "Account verification required" {
|
||||
t.Fatalf("expected account verification required message, got %q", resp.Data.Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPILoginAllowsLoginAgainAfterRevokeSameSession(t *testing.T) {
|
||||
account := testActiveAccount(t)
|
||||
clientID := "api-client"
|
||||
deviceID := "device-1"
|
||||
|
||||
rtdb := &testRefreshTokenDB{
|
||||
client: &sessionmodel.Client{
|
||||
ClientID: clientID,
|
||||
ClientSecret: "secret",
|
||||
GrantTypes: []string{"password"},
|
||||
TokenEndpointAuthMethod: "client_secret_post",
|
||||
AccountRef: account.GetID(),
|
||||
},
|
||||
}
|
||||
|
||||
var cfg middleware.TokenConfig
|
||||
cfg.Length = 16
|
||||
cfg.Expiration.Account = 1
|
||||
cfg.Expiration.Refresh = 24
|
||||
|
||||
signature := middleware.Signature{
|
||||
Algorithm: "HS256",
|
||||
PrivateKey: []byte("test-secret"),
|
||||
}
|
||||
|
||||
router := NewRouter(
|
||||
zap.NewNop(),
|
||||
"/",
|
||||
&testAccountDB{result: account},
|
||||
nil,
|
||||
rtdb,
|
||||
nil,
|
||||
chi.NewRouter(),
|
||||
&cfg,
|
||||
&signature,
|
||||
middleware.LoginRateLimits{},
|
||||
)
|
||||
|
||||
body := `{
|
||||
"login":{"login":"user@example.com","password":"correct horse battery staple"},
|
||||
"clientId":"api-client",
|
||||
"deviceId":"device-1",
|
||||
"clientSecret":"secret"
|
||||
}`
|
||||
|
||||
firstReq := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/login/api", strings.NewReader(body))
|
||||
firstRec := httptest.NewRecorder()
|
||||
router.apiLogin(firstReq)(firstRec, firstReq)
|
||||
|
||||
if firstRec.Code != http.StatusOK {
|
||||
t.Fatalf("expected first login status %d, got %d", http.StatusOK, firstRec.Code)
|
||||
}
|
||||
firstResp := decodeLoginResponse(t, firstRec.Body.Bytes())
|
||||
if firstResp.Data.RefreshToken.Token == "" {
|
||||
t.Fatal("expected first login to return refresh token")
|
||||
}
|
||||
|
||||
err := rtdb.Revoke(context.Background(), *account.GetID(), &sessionmodel.SessionIdentifier{
|
||||
ClientID: clientID,
|
||||
DeviceID: deviceID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to revoke refresh token: %v", err)
|
||||
}
|
||||
|
||||
secondReq := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/login/api", strings.NewReader(body))
|
||||
secondRec := httptest.NewRecorder()
|
||||
router.apiLogin(secondReq)(secondRec, secondReq)
|
||||
|
||||
if secondRec.Code != http.StatusOK {
|
||||
t.Fatalf("expected second login status %d, got %d", http.StatusOK, secondRec.Code)
|
||||
}
|
||||
secondResp := decodeLoginResponse(t, secondRec.Body.Bytes())
|
||||
if secondResp.Data.RefreshToken.Token == "" {
|
||||
t.Fatal("expected second login to return refresh token")
|
||||
}
|
||||
if secondResp.Data.RefreshToken.Token == firstResp.Data.RefreshToken.Token {
|
||||
t.Fatal("expected second login to issue a different refresh token after revocation")
|
||||
}
|
||||
|
||||
_, err = rtdb.GetByCRT(context.Background(), &sessionmodel.ClientRefreshToken{
|
||||
SessionIdentifier: sessionmodel.SessionIdentifier{
|
||||
ClientID: clientID,
|
||||
DeviceID: deviceID,
|
||||
},
|
||||
RefreshToken: firstResp.Data.RefreshToken.Token,
|
||||
})
|
||||
if !errors.Is(err, merrors.ErrNoData) {
|
||||
t.Fatalf("expected old refresh token to be unusable, got %v", err)
|
||||
}
|
||||
|
||||
_, err = rtdb.GetByCRT(context.Background(), &sessionmodel.ClientRefreshToken{
|
||||
SessionIdentifier: sessionmodel.SessionIdentifier{
|
||||
ClientID: clientID,
|
||||
DeviceID: deviceID,
|
||||
},
|
||||
RefreshToken: secondResp.Data.RefreshToken.Token,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected new refresh token to be active, got %v", err)
|
||||
}
|
||||
|
||||
if got := rtdb.tokenCountForSession(*account.GetID(), clientID, deviceID); got != 2 {
|
||||
t.Fatalf("expected 2 refresh token records for session, got %d", got)
|
||||
}
|
||||
if got := rtdb.activeTokenCountForSession(*account.GetID(), clientID, deviceID); got != 1 {
|
||||
t.Fatalf("expected 1 active refresh token for session, got %d", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user