package public import ( "context" 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" "time" "git.sendico.io/tech/sxchnge/product/auth/config" "git.sendico.io/tech/sxchnge/product/sessions/browser" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func newBrowserRefreshRouter(t *testing.T, expiresAt time.Time) (*PublicRouter, *testRefreshTokenDB) { t.Helper() account := testActiveAccount(t) clientID := "web" deviceID := "browser-1" rtdb := &testRefreshTokenDB{ client: &sessionmodel.Client{ClientID: clientID}, tokens: []*sessionmodel.RefreshToken{ { ClientRefreshToken: sessionmodel.ClientRefreshToken{ SessionIdentifier: sessionmodel.SessionIdentifier{ ClientID: clientID, DeviceID: deviceID, SessionVersion: "version-1", }, RefreshToken: "cookie-refresh", }, AccountRef: account.GetID(), ExpiresAt: expiresAt, }, }, } config := middleware.TokenConfig{ Expiration: middleware.TokenExpiration{Account: 1, Refresh: 720}, Length: 32, } signature := middleware.Signature{ Algorithm: "HS256", PrivateKey: []byte("browser-refresh-test-secret"), } return NewRouter( zap.NewNop(), "/api/v1", &testAccountDB{result: account}, nil, rtdb, nil, chi.NewRouter(), &config, &signature, middleware.LoginRateLimits{}, ), rtdb } func executeBrowserRefresh(t *testing.T, router *PublicRouter) *httptest.ResponseRecorder { t.Helper() body := `{"clientId":"web","deviceId":"browser-1","token":"body-must-be-ignored"}` request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "https://app.sendico.io/api/v1/accounts/refresh", strings.NewReader(body)) request.Header.Set(browser.TransportHeader, browser.TransportCookie) request.AddCookie(&http.Cookie{Name: browser.RefreshCookie, Value: "cookie-refresh"}) response := httptest.NewRecorder() router.refreshAccessToken(request)(response, request) return response } func executeNativeRefresh(t *testing.T, router *PublicRouter, token string) *httptest.ResponseRecorder { t.Helper() body := `{"clientId":"web","deviceId":"browser-1","token":"` + token + `"}` request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "https://app.sendico.io/api/v1/accounts/refresh", strings.NewReader(body)) response := httptest.NewRecorder() router.refreshAccessToken(request)(response, request) return response } func TestBrowserRefreshRestoresAccessWithoutExposingRefreshCredential(t *testing.T) { router, _ := newBrowserRefreshRouter(t, time.Now().Add(30*24*time.Hour)) response := executeBrowserRefresh(t, router) require.Equal(t, http.StatusOK, response.Code) require.NotContains(t, response.Body.String(), "cookie-refresh") require.NotEmpty(t, decodeLoginResponse(t, response.Body.Bytes()).Data.AccessToken.Token) require.Empty(t, response.Header().Get("Set-Cookie")) } func TestBrowserRefreshRotatesExpiringCookieServerSide(t *testing.T) { t.Setenv("API_ENDPOINT", "/api/v1") router, rtdb := newBrowserRefreshRouter(t, time.Now().Add(24*time.Hour)) response := executeBrowserRefresh(t, router) require.Equal(t, http.StatusOK, response.Code) require.NotContains(t, response.Body.String(), "cookie-refresh") cookies := response.Result().Cookies() require.Len(t, cookies, 1) require.Equal(t, browser.RefreshCookie, cookies[0].Name) require.NotEqual(t, "cookie-refresh", cookies[0].Value) require.Empty(t, decodeLoginResponse(t, response.Body.Bytes()).Data.RefreshToken.Token) require.Equal(t, cookies[0].Value, rtdb.tokens[0].RefreshToken) } func TestBrowserRefreshRejectsStaleCookieAndClearsIt(t *testing.T) { t.Setenv("API_ENDPOINT", "/api/v1") router, rtdb := newBrowserRefreshRouter(t, time.Now().Add(30*24*time.Hour)) rtdb.tokens = nil response := executeBrowserRefresh(t, router) require.Equal(t, http.StatusUnauthorized, response.Code) require.Contains(t, response.Body.String(), `"error":"unauthorized"`) require.NotContains(t, response.Body.String(), "cookie-refresh") cookies := response.Result().Cookies() require.Len(t, cookies, 1) require.Equal(t, browser.RefreshCookie, cookies[0].Name) require.Empty(t, cookies[0].Value) require.Equal(t, -1, cookies[0].MaxAge) require.True(t, cookies[0].HttpOnly) require.True(t, cookies[0].Secure) } func TestNativeRefreshRejectsStaleTokenWithoutCookieMutation(t *testing.T) { router, _ := newBrowserRefreshRouter(t, time.Now().Add(30*24*time.Hour)) response := executeNativeRefresh(t, router, "stale-refresh") require.Equal(t, http.StatusUnauthorized, response.Code) require.Contains(t, response.Body.String(), `"error":"unauthorized"`) require.Empty(t, response.Header().Get("Set-Cookie")) } func TestRefreshRejectsInactiveCustomerAndClientMismatch(t *testing.T) { for _, status := range []customermodel.AccountStatus{customermodel.AccountBlocked, customermodel.AccountPendingVerification} { t.Run(string(status), func(t *testing.T) { router, _ := newBrowserRefreshRouter(t, time.Now().Add(24*time.Hour)) router.db.(*testAccountDB).result.Status = status require.Equal(t, http.StatusUnauthorized, executeBrowserRefresh(t, router).Code) }) } router, sessions := newBrowserRefreshRouter(t, time.Now().Add(24*time.Hour)) other := testActiveAccount(t) sessions.client.AccountRef = &other.ID require.Equal(t, http.StatusUnauthorized, executeBrowserRefresh(t, router).Code) }