- Stripe Connect: onboarding, balance, SellerDashboardView - Interceptors: auth.ts, error.ts extracted, facade - Grafana: dashboards enriched (p50, top endpoints, 4xx, WS, commerce) - E2E commerce: product->order->review->invoice - SMOKE_TEST_V0602, RETROSPECTIVE_V0602, PAYOUT_MANUAL - Archive V0_602 scope, V0_603 placeholder, SCOPE_CONTROL v0.603 - Fix sanitizer regex (Go no backreferences) - Marketplace test schema: product_licenses, product_images, orders, licenses
298 lines
8.9 KiB
Go
298 lines
8.9 KiB
Go
package services
|
|
|
|
import (
|
|
"database/sql"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
|
|
"veza-backend-api/internal/database"
|
|
)
|
|
|
|
// Helper to setup mock DB
|
|
func setupMockDB(t *testing.T) (*database.Database, sqlmock.Sqlmock) {
|
|
db, mock, err := sqlmock.New()
|
|
require.NoError(t, err)
|
|
|
|
dbWrapper := &database.Database{
|
|
DB: db,
|
|
Logger: zap.NewNop(),
|
|
}
|
|
|
|
return dbWrapper, mock
|
|
}
|
|
|
|
func TestOAuthService_GenerateStateToken_Success(t *testing.T) {
|
|
// Setup
|
|
db, mock := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
logger := zap.NewNop()
|
|
service := &OAuthService{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
|
|
provider := "google"
|
|
redirectURL := "http://example.com"
|
|
|
|
// Expectation
|
|
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO oauth_states`)).
|
|
WithArgs(sqlmock.AnyArg(), provider, redirectURL, sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
// Execute
|
|
token, err := service.GenerateStateToken(provider, redirectURL)
|
|
|
|
// Assert
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, token)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestOAuthService_ValidateStateToken_Success(t *testing.T) {
|
|
// Setup
|
|
db, mock := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
logger := zap.NewNop()
|
|
service := &OAuthService{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
|
|
token := "valid_token"
|
|
now := time.Now()
|
|
|
|
// Expectation
|
|
rows := sqlmock.NewRows([]string{"id", "state_token", "provider", "redirect_url", "expires_at", "created_at"}).
|
|
AddRow(1, token, "google", "http://example.com", now.Add(time.Hour), now)
|
|
|
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, state_token, provider, redirect_url, expires_at, created_at FROM oauth_states WHERE state_token = $1`)).
|
|
WithArgs(token).
|
|
WillReturnRows(rows)
|
|
|
|
mock.ExpectExec(regexp.QuoteMeta(`DELETE FROM oauth_states WHERE id = $1`)).
|
|
WithArgs(1).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
// Execute
|
|
state, err := service.ValidateStateToken(token)
|
|
|
|
// Assert
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, state)
|
|
assert.Equal(t, token, state.StateToken)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestOAuthService_ValidateStateToken_NotFound(t *testing.T) {
|
|
// Setup
|
|
db, mock := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
logger := zap.NewNop()
|
|
service := &OAuthService{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
|
|
token := "invalid_token"
|
|
|
|
// Expectation
|
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, state_token, provider, redirect_url, expires_at, created_at FROM oauth_states WHERE state_token = $1`)).
|
|
WithArgs(token).
|
|
WillReturnError(sql.ErrNoRows)
|
|
|
|
// Execute
|
|
state, err := service.ValidateStateToken(token)
|
|
|
|
// Assert
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "invalid state token", err.Error())
|
|
assert.Nil(t, state)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestOAuthService_ValidateStateToken_Expired(t *testing.T) {
|
|
// Setup
|
|
db, mock := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
logger := zap.NewNop()
|
|
service := &OAuthService{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
|
|
token := "expired_token"
|
|
now := time.Now()
|
|
|
|
// Expectation
|
|
rows := sqlmock.NewRows([]string{"id", "state_token", "provider", "redirect_url", "expires_at", "created_at"}).
|
|
AddRow(1, token, "google", "http://example.com", now.Add(-time.Hour), now.Add(-2*time.Hour))
|
|
|
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, state_token, provider, redirect_url, expires_at, created_at FROM oauth_states WHERE state_token = $1`)).
|
|
WithArgs(token).
|
|
WillReturnRows(rows)
|
|
|
|
// Execute
|
|
state, err := service.ValidateStateToken(token)
|
|
|
|
// Assert
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "state token expired", err.Error())
|
|
assert.Nil(t, state)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestOAuthService_GetAuthURL_Discord(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
|
svc.InitializeConfigs("", "", "", "", "discord-client", "discord-secret", "", "", "http://localhost:8080")
|
|
|
|
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO oauth_states`)).
|
|
WithArgs(sqlmock.AnyArg(), "discord", "", sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
url, err := svc.GetAuthURL("discord")
|
|
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, url)
|
|
assert.Contains(t, url, "discord.com/api/oauth2/authorize")
|
|
assert.Contains(t, url, "identify")
|
|
assert.Contains(t, url, "email")
|
|
assert.Contains(t, url, "discord-client")
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestOAuthService_GetAuthURL_Spotify(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
|
svc.InitializeConfigs("", "", "", "", "", "", "spotify-client", "spotify-secret", "http://localhost:8080")
|
|
|
|
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO oauth_states`)).
|
|
WithArgs(sqlmock.AnyArg(), "spotify", "", sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
url, err := svc.GetAuthURL("spotify")
|
|
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, url)
|
|
assert.Contains(t, url, "accounts.spotify.com/authorize")
|
|
assert.Contains(t, url, "user-read-email")
|
|
assert.Contains(t, url, "user-read-private")
|
|
assert.Contains(t, url, "spotify-client")
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestOAuthService_GetAvailableProviders(t *testing.T) {
|
|
db, _ := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
|
providers := svc.GetAvailableProviders()
|
|
assert.Empty(t, providers)
|
|
|
|
svc.InitializeConfigs("", "", "gid", "gsec", "did", "dsec", "sid", "ssec", "http://localhost") // github, discord, spotify
|
|
providers = svc.GetAvailableProviders()
|
|
assert.Contains(t, providers, "github")
|
|
assert.Contains(t, providers, "discord")
|
|
assert.Contains(t, providers, "spotify")
|
|
assert.Len(t, providers, 3)
|
|
}
|
|
|
|
// mockOAuthTransport mocks HTTP responses for Discord/Spotify user info APIs
|
|
type mockOAuthTransport struct {
|
|
discordResponse string
|
|
spotifyResponse string
|
|
}
|
|
|
|
func (m *mockOAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
url := req.URL.String()
|
|
body := ""
|
|
if strings.Contains(url, "discord.com") {
|
|
body = m.discordResponse
|
|
} else if strings.Contains(url, "spotify.com") {
|
|
body = m.spotifyResponse
|
|
}
|
|
return &http.Response{
|
|
StatusCode: 200,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}
|
|
|
|
func TestOAuthService_GetUserInfo_Discord(t *testing.T) {
|
|
discordJSON := `{"id":"123456","username":"testuser","email":"test@example.com","avatar":"abc123"}`
|
|
transport := &mockOAuthTransport{discordResponse: discordJSON}
|
|
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}
|
|
|
|
db, mock := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
|
svc.InitializeConfigs("", "", "", "", "did", "dsec", "", "", "http://localhost")
|
|
svc.circuitBreaker = NewCircuitBreakerHTTPClient(client, "oauth-test", zap.NewNop())
|
|
|
|
user, err := svc.getUserInfo("discord", "fake-token")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "123456", user.ProviderID)
|
|
assert.Equal(t, "testuser", user.Username)
|
|
assert.Equal(t, "test@example.com", user.Email)
|
|
assert.Equal(t, "testuser", user.Name)
|
|
assert.Equal(t, "abc123", user.Avatar)
|
|
_ = mock
|
|
}
|
|
|
|
func TestOAuthService_GetUserInfo_Spotify(t *testing.T) {
|
|
spotifyJSON := `{"id":"spot123","display_name":"Spot User","email":"spot@example.com","images":[{"url":"https://avatar.url"}]}`
|
|
transport := &mockOAuthTransport{spotifyResponse: spotifyJSON}
|
|
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}
|
|
|
|
db, _ := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
|
svc.InitializeConfigs("", "", "", "", "", "", "sid", "ssec", "http://localhost")
|
|
svc.circuitBreaker = NewCircuitBreakerHTTPClient(client, "oauth-test", zap.NewNop())
|
|
|
|
user, err := svc.getUserInfo("spotify", "fake-token")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "spot123", user.ProviderID)
|
|
assert.Equal(t, "Spot User", user.Username)
|
|
assert.Equal(t, "spot@example.com", user.Email)
|
|
assert.Equal(t, "https://avatar.url", user.Avatar)
|
|
}
|
|
|
|
func TestOAuthService_GetUserInfo_Spotify_FallbackEmail(t *testing.T) {
|
|
// Spotify without email - should fallback to id@spotify.user
|
|
spotifyJSON := `{"id":"spot456","display_name":"","images":[]}`
|
|
transport := &mockOAuthTransport{spotifyResponse: spotifyJSON}
|
|
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}
|
|
|
|
db, _ := setupMockDB(t)
|
|
defer db.DB.Close()
|
|
|
|
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
|
svc.InitializeConfigs("", "", "", "", "", "", "sid", "ssec", "http://localhost")
|
|
svc.circuitBreaker = NewCircuitBreakerHTTPClient(client, "oauth-test", zap.NewNop())
|
|
|
|
user, err := svc.getUserInfo("spotify", "fake-token")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "spot456", user.ProviderID)
|
|
assert.Equal(t, "spot456", user.Username) // fallback to ID when display_name empty
|
|
assert.Equal(t, "spot456@spotify.user", user.Email)
|
|
}
|