Merge pull request #1157 from trheyi/main
Add authorized info handling and session ID retrieval in OAuth guard
This commit is contained in:
commit
1852e2cff0
8 changed files with 2061 additions and 38 deletions
|
|
@ -33,6 +33,53 @@ func (s *Service) Guard(c *gin.Context) {
|
||||||
if claims.ExpiresAt.Before(time.Now()) {
|
if claims.ExpiresAt.Before(time.Now()) {
|
||||||
s.tryAutoRefreshToken(c, claims)
|
s.tryAutoRefreshToken(c, claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set Authorized Info
|
||||||
|
s.setAuthorizedInfo(c, claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorizedInfo Get Authorized Info from context
|
||||||
|
func GetAuthorizedInfo(c *gin.Context) *types.AuthorizedInfo {
|
||||||
|
info := &types.AuthorizedInfo{}
|
||||||
|
|
||||||
|
if subject, ok := c.Get("__subject"); ok {
|
||||||
|
info.Subject = subject.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientID, ok := c.Get("__client_id"); ok {
|
||||||
|
info.ClientID = clientID.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userID, ok := c.Get("__user_id"); ok {
|
||||||
|
info.UserID = userID.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if scope, ok := c.Get("__scope"); ok {
|
||||||
|
info.Scope = scope.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Authorized Info in context
|
||||||
|
func (s *Service) setAuthorizedInfo(c *gin.Context, claims *types.TokenClaims) {
|
||||||
|
sid := s.getSessionID(c)
|
||||||
|
|
||||||
|
// Set __sid in context
|
||||||
|
if sid != "" {
|
||||||
|
c.Set("__sid", sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set __userID in context
|
||||||
|
userID, err := s.UserID(claims.ClientID, claims.Subject)
|
||||||
|
if err == nil && userID != "" {
|
||||||
|
c.Set("__user_id", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set subject scope, client_id, user_id in context
|
||||||
|
c.Set("__subject", claims.Subject)
|
||||||
|
c.Set("__scope", claims.Scope)
|
||||||
|
c.Set("__client_id", claims.ClientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) {
|
func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) {
|
||||||
|
|
@ -77,3 +124,35 @@ func (s *Service) getRefreshToken(c *gin.Context) string {
|
||||||
}
|
}
|
||||||
return strings.TrimPrefix(token, "Bearer ")
|
return strings.TrimPrefix(token, "Bearer ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get Session ID from cookies, headers, or query string
|
||||||
|
func (s *Service) getSessionID(c *gin.Context) string {
|
||||||
|
|
||||||
|
// 0. If has __sid in context, return it
|
||||||
|
sid, ok := c.Get("__sid")
|
||||||
|
if ok {
|
||||||
|
return sid.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Try to get Session ID from cookies first
|
||||||
|
if sid, err := c.Cookie("__Host-session_id"); err == nil && sid != "" {
|
||||||
|
return sid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try to get Session ID from X-Session-ID header
|
||||||
|
if sessionHeader := c.GetHeader("X-Session-ID"); sessionHeader != "" {
|
||||||
|
return sessionHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try to get Session ID from query string
|
||||||
|
if sessionQuery := c.Query("session_id"); sessionQuery != "" {
|
||||||
|
return sessionQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Try alternative query parameter names
|
||||||
|
if sessionQuery := c.Query("sid"); sessionQuery != "" {
|
||||||
|
return sessionQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -574,6 +574,15 @@ type TokenClaims struct {
|
||||||
JTI string `json:"jti,omitempty"` // JWT ID (for JWT tokens)
|
JTI string `json:"jti,omitempty"` // JWT ID (for JWT tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthorizedInfo represents authorized information
|
||||||
|
type AuthorizedInfo struct {
|
||||||
|
Subject string `json:"sub,omitempty"` // Subject identifier
|
||||||
|
ClientID string `json:"client_id"` // OAuth client ID
|
||||||
|
Scope string `json:"scope,omitempty"` // Access scope
|
||||||
|
SessionID string `json:"session_id,omitempty"` // Session ID
|
||||||
|
UserID string `json:"user_id,omitempty"` // User ID
|
||||||
|
}
|
||||||
|
|
||||||
// JWTClaims represents JWT-specific claims structure
|
// JWTClaims represents JWT-specific claims structure
|
||||||
type JWTClaims struct {
|
type JWTClaims struct {
|
||||||
jwt.StandardClaims
|
jwt.StandardClaims
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/kb"
|
"github.com/yaoapp/yao/kb"
|
||||||
"github.com/yaoapp/yao/openapi"
|
"github.com/yaoapp/yao/openapi"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
"github.com/yaoapp/yao/test"
|
"github.com/yaoapp/yao/test"
|
||||||
)
|
)
|
||||||
|
|
@ -618,6 +619,7 @@ type TokenInfo struct {
|
||||||
ExpiresIn int
|
ExpiresIn int
|
||||||
Scope string
|
Scope string
|
||||||
ClientID string
|
ClientID string
|
||||||
|
UserID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObtainAccessToken obtains an access token for testing OAuth endpoints that require authentication.
|
// ObtainAccessToken obtains an access token for testing OAuth endpoints that require authentication.
|
||||||
|
|
@ -630,19 +632,33 @@ func ObtainAccessToken(t *testing.T, serverURL, clientID, clientSecret, redirect
|
||||||
t.Fatal("OpenAPI server not initialized. Call Prepare(t) first.")
|
t.Fatal("OpenAPI server not initialized. Call Prepare(t) first.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Get authorization code with PKCE parameters
|
// Step 1: Create a test user and set up fingerprint mapping
|
||||||
authInfo := ObtainAuthorizationCode(t, serverURL, clientID, redirectURI, scope)
|
testUserID, subject := createTestUser(t, server, clientID)
|
||||||
|
|
||||||
// Step 2: Exchange authorization code for access token with PKCE code verifier
|
// Step 2: Create access token directly using the OAuth service
|
||||||
ctx := context.Background()
|
// This bypasses the authorization flow and creates a token for our test user
|
||||||
token, err := server.OAuth.Token(ctx, "authorization_code", authInfo.Code, clientID, authInfo.CodeVerifier)
|
oauthService := oauth.OAuth
|
||||||
if err != nil {
|
if oauthService == nil {
|
||||||
t.Fatalf("Failed to exchange authorization code for token: %v", err)
|
t.Fatal("Global OAuth service not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify we got a valid token
|
accessToken, err := oauthService.MakeAccessToken(clientID, scope, subject, 3600)
|
||||||
if token.AccessToken == "" {
|
if err != nil {
|
||||||
t.Fatal("Token response missing access token")
|
t.Fatalf("Failed to create access token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err := oauthService.MakeRefreshToken(clientID, scope, subject, 7200)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create refresh token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a synthetic token response
|
||||||
|
token := &types.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
ExpiresIn: 3600,
|
||||||
|
Scope: scope,
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenInfo := &TokenInfo{
|
tokenInfo := &TokenInfo{
|
||||||
|
|
@ -652,10 +668,11 @@ func ObtainAccessToken(t *testing.T, serverURL, clientID, clientSecret, redirect
|
||||||
ExpiresIn: token.ExpiresIn,
|
ExpiresIn: token.ExpiresIn,
|
||||||
Scope: token.Scope,
|
Scope: token.Scope,
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
|
UserID: testUserID, // Include the test user ID
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Obtained access token: %s (type: %s, expires_in: %d)",
|
t.Logf("Obtained access token: %s (type: %s, expires_in: %d, user_id: %s)",
|
||||||
tokenInfo.AccessToken, tokenInfo.TokenType, tokenInfo.ExpiresIn)
|
tokenInfo.AccessToken, tokenInfo.TokenType, tokenInfo.ExpiresIn, tokenInfo.UserID)
|
||||||
return tokenInfo
|
return tokenInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -681,3 +698,31 @@ func generateCodeChallenge(codeVerifier string) string {
|
||||||
// Base64 URL encode the hash without padding
|
// Base64 URL encode the hash without padding
|
||||||
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:])
|
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createTestUser creates a test user and sets up proper fingerprint mapping for OAuth authentication
|
||||||
|
func createTestUser(t *testing.T, server *openapi.OpenAPI, clientID string) (string, string) {
|
||||||
|
if server.OAuth == nil {
|
||||||
|
t.Fatal("OAuth service not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique test user ID
|
||||||
|
testUserID := fmt.Sprintf("test_user_%d", time.Now().UnixNano())
|
||||||
|
|
||||||
|
// Access the global OAuth service to set up fingerprint mapping
|
||||||
|
// The OAuth interface doesn't expose Subject method, so we need to access the concrete service
|
||||||
|
oauthService := oauth.OAuth
|
||||||
|
if oauthService == nil {
|
||||||
|
t.Fatal("Global OAuth service not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subject (fingerprint) for this user using the concrete OAuth service
|
||||||
|
// This will set up the proper fingerprint mapping: clientID:subject -> userID
|
||||||
|
subject, err := oauthService.Subject(clientID, testUserID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create user subject: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Created test user: %s with subject: %s", testUserID, subject)
|
||||||
|
return testUserID, subject
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
871
openapi/tests/user/team_test.go
Normal file
871
openapi/tests/user/team_test.go
Normal file
|
|
@ -0,0 +1,871 @@
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/yaoapp/yao/openapi"
|
||||||
|
"github.com/yaoapp/yao/openapi/tests/testutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestTeamList tests the GET /user/teams endpoint
|
||||||
|
func TestTeamList(t *testing.T) {
|
||||||
|
// Initialize test environment
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
// Get base URL from server config
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a test client for OAuth authentication
|
||||||
|
testClient := testutils.RegisterTestClient(t, "Team Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||||
|
|
||||||
|
// Obtain access token for authenticated requests
|
||||||
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
endpoint string
|
||||||
|
headers map[string]string
|
||||||
|
expectCode int
|
||||||
|
expectMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"list teams without authentication",
|
||||||
|
"/user/teams",
|
||||||
|
map[string]string{},
|
||||||
|
401,
|
||||||
|
"should require authentication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"list teams with valid token",
|
||||||
|
"/user/teams",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should return user teams",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"list teams with pagination",
|
||||||
|
"/user/teams?page=1&pagesize=10",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should handle pagination parameters",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"list teams with status filter",
|
||||||
|
"/user/teams?status=active",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should filter by status",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"list teams with name search",
|
||||||
|
"/user/teams?name=test",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should search by name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"list teams with invalid pagesize",
|
||||||
|
"/user/teams?pagesize=1000",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should limit pagesize to maximum allowed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
requestURL := serverURL + baseURL + tc.endpoint
|
||||||
|
req, err := http.NewRequest("GET", requestURL, nil)
|
||||||
|
assert.NoError(t, err, "Should create HTTP request")
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
for key, value := range tc.headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err, "HTTP request should succeed")
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err, "Should read response body")
|
||||||
|
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
// Parse response as pagination result
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &response)
|
||||||
|
assert.NoError(t, err, "Should parse JSON response")
|
||||||
|
|
||||||
|
// Check pagination structure (consistent with other modules)
|
||||||
|
if data, ok := response["data"]; ok {
|
||||||
|
assert.IsType(t, []interface{}{}, data, "Should have data array")
|
||||||
|
}
|
||||||
|
if total, ok := response["total"]; ok {
|
||||||
|
assert.IsType(t, float64(0), total, "Should have total count")
|
||||||
|
}
|
||||||
|
if page, ok := response["page"]; ok {
|
||||||
|
assert.IsType(t, float64(0), page, "Should have page number")
|
||||||
|
}
|
||||||
|
if pagesize, ok := response["pagesize"]; ok {
|
||||||
|
assert.IsType(t, float64(0), pagesize, "Should have pagesize")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Team list test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTeamCreate tests the POST /user/teams endpoint
|
||||||
|
func TestTeamCreate(t *testing.T) {
|
||||||
|
// Initialize test environment
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
// Get base URL from server config
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a test client for OAuth authentication
|
||||||
|
testClient := testutils.RegisterTestClient(t, "Team Create Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||||
|
|
||||||
|
// Obtain access token for authenticated requests
|
||||||
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
body map[string]interface{}
|
||||||
|
headers map[string]string
|
||||||
|
expectCode int
|
||||||
|
expectMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"create team without authentication",
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Test Team",
|
||||||
|
},
|
||||||
|
map[string]string{},
|
||||||
|
401,
|
||||||
|
"should require authentication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create team with valid data",
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Test Team",
|
||||||
|
"description": "A test team for unit testing",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
201,
|
||||||
|
"should create team successfully",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create team with settings",
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Team with Settings",
|
||||||
|
"description": "Team with custom settings",
|
||||||
|
"settings": map[string]interface{}{
|
||||||
|
"theme": "dark",
|
||||||
|
"visibility": "private",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
201,
|
||||||
|
"should create team with settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create team without name",
|
||||||
|
map[string]interface{}{
|
||||||
|
"description": "Team without name",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
"should require team name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create team with empty name",
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
"should require non-empty team name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create team with invalid JSON",
|
||||||
|
nil, // Will send invalid JSON
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
"should handle invalid JSON",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
requestURL := serverURL + baseURL + "/user/teams"
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if tc.body == nil {
|
||||||
|
// Send invalid JSON for invalid JSON test case
|
||||||
|
req, err = http.NewRequest("POST", requestURL, bytes.NewBufferString("invalid json"))
|
||||||
|
} else {
|
||||||
|
bodyBytes, _ := json.Marshal(tc.body)
|
||||||
|
req, err = http.NewRequest("POST", requestURL, bytes.NewBuffer(bodyBytes))
|
||||||
|
}
|
||||||
|
assert.NoError(t, err, "Should create HTTP request")
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
for key, value := range tc.headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err, "HTTP request should succeed")
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err, "Should read response body")
|
||||||
|
|
||||||
|
if resp.StatusCode == 201 {
|
||||||
|
// Parse response as team object
|
||||||
|
var team map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &team)
|
||||||
|
assert.NoError(t, err, "Should parse JSON response")
|
||||||
|
|
||||||
|
// Verify team structure
|
||||||
|
assert.Contains(t, team, "id", "Should have team ID")
|
||||||
|
assert.Contains(t, team, "team_id", "Should have team_id")
|
||||||
|
assert.Contains(t, team, "name", "Should have team name")
|
||||||
|
assert.Contains(t, team, "owner_id", "Should have owner_id")
|
||||||
|
assert.Contains(t, team, "status", "Should have status")
|
||||||
|
assert.Contains(t, team, "created_at", "Should have created_at")
|
||||||
|
assert.Contains(t, team, "updated_at", "Should have updated_at")
|
||||||
|
|
||||||
|
// Verify values
|
||||||
|
if tc.body != nil {
|
||||||
|
if name, ok := tc.body["name"]; ok {
|
||||||
|
assert.Equal(t, name, team["name"], "Should have correct team name")
|
||||||
|
}
|
||||||
|
if description, ok := tc.body["description"]; ok {
|
||||||
|
assert.Equal(t, description, team["description"], "Should have correct description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Team create test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTeamGet tests the GET /user/teams/:team_id endpoint
|
||||||
|
func TestTeamGet(t *testing.T) {
|
||||||
|
// Initialize test environment
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
// Get base URL from server config
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a test client for OAuth authentication
|
||||||
|
testClient := testutils.RegisterTestClient(t, "Team Get Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||||
|
|
||||||
|
// Obtain access token for authenticated requests
|
||||||
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
// First create a team to test with
|
||||||
|
createTeamBody := map[string]interface{}{
|
||||||
|
"name": "Get Test Team",
|
||||||
|
"description": "Team for testing get functionality",
|
||||||
|
"settings": map[string]interface{}{
|
||||||
|
"theme": "light",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
createReq := createTeamRequest(t, serverURL+baseURL+"/user/teams", createTeamBody, tokenInfo.AccessToken)
|
||||||
|
createResp, err := (&http.Client{}).Do(createReq)
|
||||||
|
assert.NoError(t, err, "Should create test team")
|
||||||
|
defer createResp.Body.Close()
|
||||||
|
|
||||||
|
var createdTeam map[string]interface{}
|
||||||
|
if createResp.StatusCode == 201 {
|
||||||
|
createBody, _ := io.ReadAll(createResp.Body)
|
||||||
|
json.Unmarshal(createBody, &createdTeam)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
teamID string
|
||||||
|
headers map[string]string
|
||||||
|
expectCode int
|
||||||
|
expectMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"get team without authentication",
|
||||||
|
getTeamID(createdTeam),
|
||||||
|
map[string]string{},
|
||||||
|
401,
|
||||||
|
"should require authentication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"get existing team",
|
||||||
|
getTeamID(createdTeam),
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should return team details",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"get non-existent team",
|
||||||
|
"non-existent-team-id",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
404,
|
||||||
|
"should return not found for non-existent team",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"get team with empty team_id returns team list",
|
||||||
|
"",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should return team list when team_id is empty",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
endpoint := "/user/teams"
|
||||||
|
if tc.teamID != "" {
|
||||||
|
endpoint += "/" + tc.teamID
|
||||||
|
}
|
||||||
|
requestURL := serverURL + baseURL + endpoint
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", requestURL, nil)
|
||||||
|
assert.NoError(t, err, "Should create HTTP request")
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
for key, value := range tc.headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err, "HTTP request should succeed")
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err, "Should read response body")
|
||||||
|
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
if tc.teamID == "" {
|
||||||
|
// Parse response as team list (pagination result)
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &response)
|
||||||
|
assert.NoError(t, err, "Should parse JSON response")
|
||||||
|
|
||||||
|
// Check pagination structure
|
||||||
|
assert.Contains(t, response, "data", "Should have data array")
|
||||||
|
assert.Contains(t, response, "total", "Should have total count")
|
||||||
|
assert.Contains(t, response, "page", "Should have page number")
|
||||||
|
assert.Contains(t, response, "pagesize", "Should have pagesize")
|
||||||
|
} else {
|
||||||
|
// Parse response as team detail object
|
||||||
|
var team map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &team)
|
||||||
|
assert.NoError(t, err, "Should parse JSON response")
|
||||||
|
|
||||||
|
// Verify team detail structure
|
||||||
|
assert.Contains(t, team, "id", "Should have team ID")
|
||||||
|
assert.Contains(t, team, "team_id", "Should have team_id")
|
||||||
|
assert.Contains(t, team, "name", "Should have team name")
|
||||||
|
assert.Contains(t, team, "description", "Should have description")
|
||||||
|
assert.Contains(t, team, "owner_id", "Should have owner_id")
|
||||||
|
assert.Contains(t, team, "status", "Should have status")
|
||||||
|
assert.Contains(t, team, "settings", "Should have settings")
|
||||||
|
assert.Contains(t, team, "created_at", "Should have created_at")
|
||||||
|
assert.Contains(t, team, "updated_at", "Should have updated_at")
|
||||||
|
|
||||||
|
// Verify values match created team
|
||||||
|
assert.Equal(t, "Get Test Team", team["name"], "Should have correct team name")
|
||||||
|
assert.Equal(t, "Team for testing get functionality", team["description"], "Should have correct description")
|
||||||
|
if settings, ok := team["settings"].(map[string]interface{}); ok {
|
||||||
|
assert.Equal(t, "light", settings["theme"], "Should have correct theme setting")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Team get test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTeamUpdate tests the PUT /user/teams/:team_id endpoint
|
||||||
|
func TestTeamUpdate(t *testing.T) {
|
||||||
|
// Initialize test environment
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
// Get base URL from server config
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a test client for OAuth authentication
|
||||||
|
testClient := testutils.RegisterTestClient(t, "Team Update Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||||
|
|
||||||
|
// Obtain access token for authenticated requests
|
||||||
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
// Create a team to test updates
|
||||||
|
createTeamBody := map[string]interface{}{
|
||||||
|
"name": "Update Test Team",
|
||||||
|
"description": "Team for testing update functionality",
|
||||||
|
}
|
||||||
|
|
||||||
|
createReq := createTeamRequest(t, serverURL+baseURL+"/user/teams", createTeamBody, tokenInfo.AccessToken)
|
||||||
|
createResp, err := (&http.Client{}).Do(createReq)
|
||||||
|
assert.NoError(t, err, "Should create test team")
|
||||||
|
defer createResp.Body.Close()
|
||||||
|
|
||||||
|
var createdTeam map[string]interface{}
|
||||||
|
if createResp.StatusCode == 201 {
|
||||||
|
createBody, _ := io.ReadAll(createResp.Body)
|
||||||
|
json.Unmarshal(createBody, &createdTeam)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
teamID string
|
||||||
|
body map[string]interface{}
|
||||||
|
headers map[string]string
|
||||||
|
expectCode int
|
||||||
|
expectMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"update team without authentication",
|
||||||
|
getTeamID(createdTeam),
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Updated Name",
|
||||||
|
},
|
||||||
|
map[string]string{},
|
||||||
|
401,
|
||||||
|
"should require authentication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update team name",
|
||||||
|
getTeamID(createdTeam),
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Updated Team Name",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should update team name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update team description",
|
||||||
|
getTeamID(createdTeam),
|
||||||
|
map[string]interface{}{
|
||||||
|
"description": "Updated description",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should update team description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update team settings",
|
||||||
|
getTeamID(createdTeam),
|
||||||
|
map[string]interface{}{
|
||||||
|
"settings": map[string]interface{}{
|
||||||
|
"theme": "dark",
|
||||||
|
"visibility": "public",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should update team settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update non-existent team",
|
||||||
|
"non-existent-team-id",
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Updated Name",
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
404,
|
||||||
|
"should return not found for non-existent team",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"update team with invalid JSON",
|
||||||
|
getTeamID(createdTeam),
|
||||||
|
nil, // Will send invalid JSON
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
"should handle invalid JSON",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if tc.body == nil {
|
||||||
|
// Send invalid JSON for invalid JSON test case
|
||||||
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBufferString("invalid json"))
|
||||||
|
} else {
|
||||||
|
bodyBytes, _ := json.Marshal(tc.body)
|
||||||
|
req, err = http.NewRequest("PUT", requestURL, bytes.NewBuffer(bodyBytes))
|
||||||
|
}
|
||||||
|
assert.NoError(t, err, "Should create HTTP request")
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
for key, value := range tc.headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err, "HTTP request should succeed")
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err, "Should read response body")
|
||||||
|
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
// Parse response as updated team object
|
||||||
|
var team map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &team)
|
||||||
|
assert.NoError(t, err, "Should parse JSON response")
|
||||||
|
|
||||||
|
// Verify updated values
|
||||||
|
if tc.body != nil {
|
||||||
|
if name, ok := tc.body["name"]; ok {
|
||||||
|
assert.Equal(t, name, team["name"], "Should have updated team name")
|
||||||
|
}
|
||||||
|
if description, ok := tc.body["description"]; ok {
|
||||||
|
assert.Equal(t, description, team["description"], "Should have updated description")
|
||||||
|
}
|
||||||
|
if settings, ok := tc.body["settings"]; ok {
|
||||||
|
assert.Equal(t, settings, team["settings"], "Should have updated settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Team update test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTeamDelete tests the DELETE /user/teams/:team_id endpoint
|
||||||
|
func TestTeamDelete(t *testing.T) {
|
||||||
|
// Initialize test environment
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
// Get base URL from server config
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a test client for OAuth authentication
|
||||||
|
testClient := testutils.RegisterTestClient(t, "Team Delete Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||||
|
|
||||||
|
// Obtain access token for authenticated requests
|
||||||
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
// Create teams to test deletion
|
||||||
|
createTeam := func(name string) map[string]interface{} {
|
||||||
|
createTeamBody := map[string]interface{}{
|
||||||
|
"name": name,
|
||||||
|
"description": "Team for testing delete functionality",
|
||||||
|
}
|
||||||
|
|
||||||
|
createReq := createTeamRequest(t, serverURL+baseURL+"/user/teams", createTeamBody, tokenInfo.AccessToken)
|
||||||
|
createResp, err := (&http.Client{}).Do(createReq)
|
||||||
|
assert.NoError(t, err, "Should create test team")
|
||||||
|
defer createResp.Body.Close()
|
||||||
|
|
||||||
|
var createdTeam map[string]interface{}
|
||||||
|
if createResp.StatusCode == 201 {
|
||||||
|
createBody, _ := io.ReadAll(createResp.Body)
|
||||||
|
json.Unmarshal(createBody, &createdTeam)
|
||||||
|
}
|
||||||
|
return createdTeam
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
teamID string
|
||||||
|
headers map[string]string
|
||||||
|
expectCode int
|
||||||
|
expectMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"delete team without authentication",
|
||||||
|
getTeamID(createTeam("Delete Test Team 1")),
|
||||||
|
map[string]string{},
|
||||||
|
401,
|
||||||
|
"should require authentication",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"delete existing team",
|
||||||
|
getTeamID(createTeam("Delete Test Team 2")),
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"should delete team successfully",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"delete non-existent team",
|
||||||
|
"non-existent-team-id",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
404,
|
||||||
|
"should return not found for non-existent team",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID
|
||||||
|
|
||||||
|
req, err := http.NewRequest("DELETE", requestURL, nil)
|
||||||
|
assert.NoError(t, err, "Should create HTTP request")
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
for key, value := range tc.headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err, "HTTP request should succeed")
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err, "Should read response body")
|
||||||
|
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
// Parse response as success message
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &response)
|
||||||
|
assert.NoError(t, err, "Should parse JSON response")
|
||||||
|
|
||||||
|
assert.Contains(t, response, "message", "Should have success message")
|
||||||
|
assert.Equal(t, "Team deleted successfully", response["message"], "Should have correct success message")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Team delete test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTeamAuthenticationEdgeCases tests authentication and authorization edge cases
|
||||||
|
func TestTeamAuthenticationEdgeCases(t *testing.T) {
|
||||||
|
// Initialize test environment
|
||||||
|
serverURL := testutils.Prepare(t)
|
||||||
|
defer testutils.Clean()
|
||||||
|
|
||||||
|
// Get base URL from server config
|
||||||
|
baseURL := ""
|
||||||
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
||||||
|
baseURL = openapi.Server.Config.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a test client for OAuth authentication
|
||||||
|
testClient := testutils.RegisterTestClient(t, "Team Auth Test Client", []string{"https://localhost/callback"})
|
||||||
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||||
|
|
||||||
|
// Obtain access token for authenticated requests
|
||||||
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, testClient.ClientID, testClient.ClientSecret, "https://localhost/callback", "openid profile")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
endpoint string
|
||||||
|
method string
|
||||||
|
headers map[string]string
|
||||||
|
expectCode int
|
||||||
|
expectMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"invalid bearer token format",
|
||||||
|
"/user/teams",
|
||||||
|
"GET",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer invalid-token",
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
"should reject invalid token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"missing bearer prefix",
|
||||||
|
"/user/teams",
|
||||||
|
"GET",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": tokenInfo.AccessToken,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
"may accept token without Bearer prefix (implementation dependent)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expired token simulation",
|
||||||
|
"/user/teams",
|
||||||
|
"GET",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Bearer expired.token.here",
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
"should reject expired token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"malformed authorization header",
|
||||||
|
"/user/teams",
|
||||||
|
"GET",
|
||||||
|
map[string]string{
|
||||||
|
"Authorization": "Malformed",
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
"should reject malformed header",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
requestURL := serverURL + baseURL + tc.endpoint
|
||||||
|
|
||||||
|
req, err := http.NewRequest(tc.method, requestURL, nil)
|
||||||
|
assert.NoError(t, err, "Should create HTTP request")
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
for key, value := range tc.headers {
|
||||||
|
req.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
assert.NoError(t, err, "HTTP request should succeed")
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, tc.expectCode, resp.StatusCode, "Expected status code %d for %s", tc.expectCode, tc.name)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NoError(t, err, "Should read response body")
|
||||||
|
|
||||||
|
t.Logf("Auth edge case test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
// createTeamRequest creates a POST request for team creation
|
||||||
|
func createTeamRequest(t *testing.T, url string, body map[string]interface{}, accessToken string) *http.Request {
|
||||||
|
bodyBytes, err := json.Marshal(body)
|
||||||
|
assert.NoError(t, err, "Should marshal team creation body")
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
|
||||||
|
assert.NoError(t, err, "Should create team creation request")
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTeamID extracts team_id from created team response
|
||||||
|
func getTeamID(team map[string]interface{}) string {
|
||||||
|
if team == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if teamID, ok := team["team_id"].(string); ok {
|
||||||
|
return teamID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
844
openapi/user/team.go
Normal file
844
openapi/user/team.go
Normal file
|
|
@ -0,0 +1,844 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/gou/model"
|
||||||
|
"github.com/yaoapp/gou/process"
|
||||||
|
"github.com/yaoapp/kun/exception"
|
||||||
|
"github.com/yaoapp/kun/log"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||||
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Team Management Handlers
|
||||||
|
|
||||||
|
// GinTeamList handles GET /teams - Get user teams
|
||||||
|
func GinTeamList(c *gin.Context) {
|
||||||
|
// Get authorized user info
|
||||||
|
authInfo := oauth.GetAuthorizedInfo(c)
|
||||||
|
if authInfo == nil || authInfo.UserID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidClient.Code,
|
||||||
|
ErrorDescription: "User not authenticated",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pagination parameters
|
||||||
|
page := 1
|
||||||
|
pagesize := 20
|
||||||
|
|
||||||
|
if p := c.Query("page"); p != "" {
|
||||||
|
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
|
||||||
|
page = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ps := c.Query("pagesize"); ps != "" {
|
||||||
|
if parsed, err := strconv.Atoi(ps); err == nil && parsed > 0 && parsed <= 100 {
|
||||||
|
pagesize = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get user provider: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to initialize user provider",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query parameters
|
||||||
|
param := model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "owner_id", Value: authInfo.UserID},
|
||||||
|
},
|
||||||
|
Orders: []model.QueryOrder{
|
||||||
|
{Column: "created_at", Option: "desc"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add status filter if provided
|
||||||
|
if status := c.Query("status"); status != "" {
|
||||||
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||||
|
Column: "status",
|
||||||
|
Value: status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add name search if provided
|
||||||
|
if name := c.Query("name"); name != "" {
|
||||||
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||||
|
Column: "name",
|
||||||
|
Value: "%" + name + "%",
|
||||||
|
OP: "like",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get paginated teams
|
||||||
|
result, err := provider.PaginateTeams(c.Request.Context(), param, page, pagesize)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get user teams: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to retrieve teams",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the paginated result directly (consistent with other modules)
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinTeamGet handles GET /teams/:team_id - Get user team details
|
||||||
|
func GinTeamGet(c *gin.Context) {
|
||||||
|
// Get authorized user info
|
||||||
|
authInfo := oauth.GetAuthorizedInfo(c)
|
||||||
|
if authInfo == nil || authInfo.UserID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidClient.Code,
|
||||||
|
ErrorDescription: "User not authenticated",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID := c.Param("team_id")
|
||||||
|
if teamID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Team ID is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get user provider: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to initialize user provider",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get team details
|
||||||
|
teamData, err := provider.GetTeamDetail(c.Request.Context(), teamID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get team details: %v", err)
|
||||||
|
// Check if it's a "team not found" error
|
||||||
|
if err.Error() == "team not found" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Team not found",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
} else {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to retrieve team details",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user owns this team
|
||||||
|
ownerID := toString(teamData["owner_id"])
|
||||||
|
if ownerID != authInfo.UserID {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: "Access denied: you don't own this team",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to response format
|
||||||
|
team := mapToTeamDetailResponse(teamData)
|
||||||
|
c.JSON(http.StatusOK, team)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinTeamCreate handles POST /teams - Create user team
|
||||||
|
func GinTeamCreate(c *gin.Context) {
|
||||||
|
// Get authorized user info
|
||||||
|
authInfo := oauth.GetAuthorizedInfo(c)
|
||||||
|
if authInfo == nil || authInfo.UserID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidClient.Code,
|
||||||
|
ErrorDescription: "User not authenticated",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var req CreateTeamRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get user provider: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to initialize user provider",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare team data
|
||||||
|
teamData := maps.MapStrAny{
|
||||||
|
"name": req.Name,
|
||||||
|
"description": req.Description,
|
||||||
|
"owner_id": authInfo.UserID,
|
||||||
|
"status": "active",
|
||||||
|
"is_verified": false,
|
||||||
|
"created_at": time.Now(),
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add settings if provided
|
||||||
|
if req.Settings != nil {
|
||||||
|
teamData["settings"] = req.Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create team
|
||||||
|
teamID, err := provider.CreateTeam(c.Request.Context(), teamData)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to create team: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to create team",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the created team details
|
||||||
|
createdTeam, err := provider.GetTeamDetail(c.Request.Context(), teamID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get created team details: %v", err)
|
||||||
|
// Return basic response if we can't get details
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"team_id": teamID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to response format
|
||||||
|
team := mapToTeamDetailResponse(createdTeam)
|
||||||
|
c.JSON(http.StatusCreated, team)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinTeamUpdate handles PUT /teams/:team_id - Update user team
|
||||||
|
func GinTeamUpdate(c *gin.Context) {
|
||||||
|
// Get authorized user info
|
||||||
|
authInfo := oauth.GetAuthorizedInfo(c)
|
||||||
|
if authInfo == nil || authInfo.UserID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidClient.Code,
|
||||||
|
ErrorDescription: "User not authenticated",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID := c.Param("team_id")
|
||||||
|
if teamID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Team ID is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var req UpdateTeamRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get user provider: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to initialize user provider",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if team exists and user owns it
|
||||||
|
teamData, err := provider.GetTeam(c.Request.Context(), teamID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get team: %v", err)
|
||||||
|
// Check if it's a "team not found" error
|
||||||
|
if err.Error() == "team not found" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Team not found",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
} else {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to get team",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
ownerID := toString(teamData["owner_id"])
|
||||||
|
if ownerID != authInfo.UserID {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: "Access denied: you don't own this team",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare update data
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != "" {
|
||||||
|
updateData["name"] = req.Name
|
||||||
|
}
|
||||||
|
if req.Description != "" {
|
||||||
|
updateData["description"] = req.Description
|
||||||
|
}
|
||||||
|
if req.Settings != nil {
|
||||||
|
updateData["settings"] = req.Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update team
|
||||||
|
err = provider.UpdateTeam(c.Request.Context(), teamID, updateData)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to update team: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to update team",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get updated team details
|
||||||
|
updatedTeam, err := provider.GetTeamDetail(c.Request.Context(), teamID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get updated team details: %v", err)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Team updated successfully"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to response format
|
||||||
|
team := mapToTeamDetailResponse(updatedTeam)
|
||||||
|
c.JSON(http.StatusOK, team)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinTeamDelete handles DELETE /teams/:team_id - Delete user team
|
||||||
|
func GinTeamDelete(c *gin.Context) {
|
||||||
|
// Get authorized user info
|
||||||
|
authInfo := oauth.GetAuthorizedInfo(c)
|
||||||
|
if authInfo == nil || authInfo.UserID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidClient.Code,
|
||||||
|
ErrorDescription: "User not authenticated",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamID := c.Param("team_id")
|
||||||
|
if teamID == "" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Team ID is required",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get user provider: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to initialize user provider",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if team exists and user owns it
|
||||||
|
teamData, err := provider.GetTeam(c.Request.Context(), teamID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to get team: %v", err)
|
||||||
|
// Check if it's a "team not found" error
|
||||||
|
if err.Error() == "team not found" {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrInvalidRequest.Code,
|
||||||
|
ErrorDescription: "Team not found",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||||
|
} else {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to get team",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
ownerID := toString(teamData["owner_id"])
|
||||||
|
if ownerID != authInfo.UserID {
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrAccessDenied.Code,
|
||||||
|
ErrorDescription: "Access denied: you don't own this team",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete team
|
||||||
|
err = provider.DeleteTeam(c.Request.Context(), teamID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to delete team: %v", err)
|
||||||
|
errorResp := &response.ErrorResponse{
|
||||||
|
Code: response.ErrServerError.Code,
|
||||||
|
ErrorDescription: "Failed to delete team",
|
||||||
|
}
|
||||||
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Team deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yao Process Handlers (for Yao application calls)
|
||||||
|
|
||||||
|
// ProcessTeamList user.team.list Team list processor
|
||||||
|
// Args[0] map: Query parameters {"status": "active", "name": "search", "page": 1, "pagesize": 20}
|
||||||
|
// Return: map: Paginated team list
|
||||||
|
func ProcessTeamList(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(1)
|
||||||
|
|
||||||
|
// Get user_id from session
|
||||||
|
userIDStr := GetUserIDFromSession(process)
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
queryMap := process.ArgsMap(0)
|
||||||
|
|
||||||
|
// Build query parameters
|
||||||
|
param := model.QueryParam{}
|
||||||
|
|
||||||
|
// Add filters
|
||||||
|
if status, ok := queryMap["status"].(string); ok && status != "" {
|
||||||
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||||
|
Column: "status",
|
||||||
|
Value: status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if name, ok := queryMap["name"].(string); ok && name != "" {
|
||||||
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||||
|
Column: "name",
|
||||||
|
Value: "%" + name + "%",
|
||||||
|
OP: "like",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pagination
|
||||||
|
page := 1
|
||||||
|
pagesize := 20
|
||||||
|
|
||||||
|
if p, ok := queryMap["page"]; ok {
|
||||||
|
if pageInt, ok := p.(int); ok && pageInt > 0 {
|
||||||
|
page = pageInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ps, ok := queryMap["pagesize"]; ok {
|
||||||
|
if pagesizeInt, ok := ps.(int); ok && pagesizeInt > 0 && pagesizeInt <= 100 {
|
||||||
|
pagesize = pagesizeInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get context
|
||||||
|
ctx := process.Context
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call business logic
|
||||||
|
result, err := teamList(ctx, userIDStr, param, page, pagesize)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("failed to list teams: %s", 500, err.Error()).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessTeamGet user.team.get Team get processor
|
||||||
|
// Args[0] string: team_id
|
||||||
|
// Return: map: Team details
|
||||||
|
func ProcessTeamGet(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(1)
|
||||||
|
|
||||||
|
// Get user_id from session
|
||||||
|
userIDStr := GetUserIDFromSession(process)
|
||||||
|
|
||||||
|
teamID := process.ArgsString(0)
|
||||||
|
if teamID == "" {
|
||||||
|
exception.New("team_id is required", 400).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get context
|
||||||
|
ctx := process.Context
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call business logic
|
||||||
|
result, err := teamGet(ctx, userIDStr, teamID)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("failed to get team: %s", 500, err.Error()).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessTeamCreate user.team.create Team create processor
|
||||||
|
// Args[0] map: Team data {"name": "Team Name", "description": "Description", "settings": {...}}
|
||||||
|
// Return: map: {"team_id": "created_team_id"}
|
||||||
|
func ProcessTeamCreate(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(1)
|
||||||
|
|
||||||
|
// Get user_id from session
|
||||||
|
userIDStr := GetUserIDFromSession(process)
|
||||||
|
|
||||||
|
teamData := maps.MapStrAny(process.ArgsMap(0))
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if _, ok := teamData["name"]; !ok {
|
||||||
|
exception.New("name is required", 400).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get context
|
||||||
|
ctx := process.Context
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call business logic
|
||||||
|
teamID, err := teamCreate(ctx, userIDStr, teamData)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("failed to create team: %s", 500, err.Error()).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"team_id": teamID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessTeamUpdate user.team.update Team update processor
|
||||||
|
// Args[0] string: team_id
|
||||||
|
// Args[1] map: Update data {"name": "New Name", "description": "New Description", "settings": {...}}
|
||||||
|
// Return: map: {"message": "success"}
|
||||||
|
func ProcessTeamUpdate(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(2)
|
||||||
|
|
||||||
|
// Get user_id from session
|
||||||
|
userIDStr := GetUserIDFromSession(process)
|
||||||
|
|
||||||
|
teamID := process.ArgsString(0)
|
||||||
|
updateData := maps.MapStrAny(process.ArgsMap(1))
|
||||||
|
|
||||||
|
if teamID == "" {
|
||||||
|
exception.New("team_id is required", 400).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get context
|
||||||
|
ctx := process.Context
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call business logic
|
||||||
|
err := teamUpdate(ctx, userIDStr, teamID, updateData)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("failed to update team: %s", 500, err.Error()).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"message": "success",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessTeamDelete user.team.delete Team delete processor
|
||||||
|
// Args[0] string: team_id
|
||||||
|
// Return: map: {"message": "success"}
|
||||||
|
func ProcessTeamDelete(process *process.Process) interface{} {
|
||||||
|
process.ValidateArgNums(1)
|
||||||
|
|
||||||
|
// Get user_id from session
|
||||||
|
userIDStr := GetUserIDFromSession(process)
|
||||||
|
|
||||||
|
teamID := process.ArgsString(0)
|
||||||
|
if teamID == "" {
|
||||||
|
exception.New("team_id is required", 400).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get context
|
||||||
|
ctx := process.Context
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call business logic
|
||||||
|
err := teamDelete(ctx, userIDStr, teamID)
|
||||||
|
if err != nil {
|
||||||
|
exception.New("failed to delete team: %s", 500, err.Error()).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"message": "success",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private Business Logic Functions (internal use only)
|
||||||
|
|
||||||
|
// teamList handles the business logic for listing user teams
|
||||||
|
func teamList(ctx context.Context, userID string, param model.QueryParam, page, pagesize int) (maps.MapStr, error) {
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add owner filter to query parameters
|
||||||
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
||||||
|
Column: "owner_id",
|
||||||
|
Value: userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set default ordering if not provided
|
||||||
|
if len(param.Orders) == 0 {
|
||||||
|
param.Orders = []model.QueryOrder{
|
||||||
|
{Column: "created_at", Option: "desc"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get paginated teams
|
||||||
|
result, err := provider.PaginateTeams(ctx, param, page, pagesize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve teams: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// teamGet handles the business logic for getting a specific user team
|
||||||
|
func teamGet(ctx context.Context, userID, teamID string) (maps.MapStrAny, error) {
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get team details
|
||||||
|
teamData, err := provider.GetTeamDetail(ctx, teamID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to retrieve team details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user owns this team
|
||||||
|
ownerID := toString(teamData["owner_id"])
|
||||||
|
if ownerID != userID {
|
||||||
|
return nil, fmt.Errorf("access denied: user does not own this team")
|
||||||
|
}
|
||||||
|
|
||||||
|
return teamData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// teamCreate handles the business logic for creating a user team
|
||||||
|
func teamCreate(ctx context.Context, userID string, teamData maps.MapStrAny) (string, error) {
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get user provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set owner and default values
|
||||||
|
teamData["owner_id"] = userID
|
||||||
|
teamData["status"] = "active"
|
||||||
|
teamData["is_verified"] = false
|
||||||
|
teamData["created_at"] = time.Now()
|
||||||
|
teamData["updated_at"] = time.Now()
|
||||||
|
|
||||||
|
// Create team
|
||||||
|
teamID, err := provider.CreateTeam(ctx, teamData)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create team: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return teamID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// teamUpdate handles the business logic for updating a user team
|
||||||
|
func teamUpdate(ctx context.Context, userID, teamID string, updateData maps.MapStrAny) error {
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if team exists and user owns it
|
||||||
|
teamData, err := provider.GetTeam(ctx, teamID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("team not found or access denied: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
ownerID := toString(teamData["owner_id"])
|
||||||
|
if ownerID != userID {
|
||||||
|
return fmt.Errorf("access denied: user does not own this team")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add updated_at timestamp
|
||||||
|
updateData["updated_at"] = time.Now()
|
||||||
|
|
||||||
|
// Update team
|
||||||
|
err = provider.UpdateTeam(ctx, teamID, updateData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update team: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// teamDelete handles the business logic for deleting a user team
|
||||||
|
func teamDelete(ctx context.Context, userID, teamID string) error {
|
||||||
|
// Get user provider instance
|
||||||
|
provider, err := getUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if team exists and user owns it
|
||||||
|
teamData, err := provider.GetTeam(ctx, teamID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("team not found or access denied: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
ownerID := toString(teamData["owner_id"])
|
||||||
|
if ownerID != userID {
|
||||||
|
return fmt.Errorf("access denied: user does not own this team")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete team
|
||||||
|
err = provider.DeleteTeam(ctx, teamID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete team: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private Helper Functions (internal use only)
|
||||||
|
|
||||||
|
// getUserProvider gets the user provider from the global OAuth service
|
||||||
|
func getUserProvider() (*user.DefaultUser, error) {
|
||||||
|
// Check if global OAuth service is initialized
|
||||||
|
if oauth.OAuth == nil {
|
||||||
|
return nil, fmt.Errorf("OAuth service not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user provider from OAuth service
|
||||||
|
userProvider, err := oauth.OAuth.GetUserProvider()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type assert to DefaultUser (this should be safe based on the OAuth service implementation)
|
||||||
|
if defaultUser, ok := userProvider.(*user.DefaultUser); ok {
|
||||||
|
return defaultUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("user provider is not of type DefaultUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapToTeamResponse converts a map to TeamResponse
|
||||||
|
func mapToTeamResponse(data maps.MapStr) TeamResponse {
|
||||||
|
team := TeamResponse{
|
||||||
|
ID: toInt64(data["id"]),
|
||||||
|
TeamID: toString(data["team_id"]),
|
||||||
|
Name: toString(data["name"]),
|
||||||
|
Description: toString(data["description"]),
|
||||||
|
OwnerID: toString(data["owner_id"]),
|
||||||
|
Status: toString(data["status"]),
|
||||||
|
IsVerified: toBool(data["is_verified"]),
|
||||||
|
VerifiedBy: toString(data["verified_by"]),
|
||||||
|
VerifiedAt: toTimeString(data["verified_at"]),
|
||||||
|
CreatedAt: toTimeString(data["created_at"]),
|
||||||
|
UpdatedAt: toTimeString(data["updated_at"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
return team
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapToTeamDetailResponse converts a map to TeamDetailResponse
|
||||||
|
func mapToTeamDetailResponse(data maps.MapStr) TeamDetailResponse {
|
||||||
|
team := TeamDetailResponse{
|
||||||
|
TeamResponse: mapToTeamResponse(data),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add settings if available
|
||||||
|
if settings, ok := data["settings"]; ok {
|
||||||
|
if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||||
|
team.Settings = settingsMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return team
|
||||||
|
}
|
||||||
|
|
@ -201,26 +201,40 @@ const (
|
||||||
UserInfoSourceAccessToken = "access_token" // Extract user info from access token response
|
UserInfoSourceAccessToken = "access_token" // Extract user info from access token response
|
||||||
)
|
)
|
||||||
|
|
||||||
// toBool converts various types to boolean
|
// ==== Team API Types ====
|
||||||
// Supports: bool, int, int64, float64, string
|
|
||||||
// Returns false for nil or unsupported types
|
|
||||||
func toBool(v interface{}) bool {
|
|
||||||
if v == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val := v.(type) {
|
// TeamResponse represents a team in API responses
|
||||||
case bool:
|
type TeamResponse struct {
|
||||||
return val
|
ID int64 `json:"id"`
|
||||||
case int:
|
TeamID string `json:"team_id"`
|
||||||
return val != 0
|
Name string `json:"name"`
|
||||||
case int64:
|
Description string `json:"description,omitempty"`
|
||||||
return val != 0
|
OwnerID string `json:"owner_id"`
|
||||||
case float64:
|
Status string `json:"status"`
|
||||||
return val != 0
|
IsVerified bool `json:"is_verified"`
|
||||||
case string:
|
VerifiedBy string `json:"verified_by,omitempty"`
|
||||||
return val == "true" || val == "1"
|
VerifiedAt string `json:"verified_at,omitempty"`
|
||||||
default:
|
CreatedAt string `json:"created_at"`
|
||||||
return false
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TeamDetailResponse represents detailed team information
|
||||||
|
type TeamDetailResponse struct {
|
||||||
|
TeamResponse
|
||||||
|
// Add additional fields that are only included in detailed responses
|
||||||
|
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTeamRequest represents the request to create a team
|
||||||
|
type CreateTeamRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTeamRequest represents the request to update a team
|
||||||
|
type UpdateTeamRequest struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,21 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/gou/process"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Register user process handlers
|
||||||
|
process.RegisterGroup("user", map[string]process.Handler{
|
||||||
|
"team.list": ProcessTeamList,
|
||||||
|
"team.get": ProcessTeamGet,
|
||||||
|
"team.create": ProcessTeamCreate,
|
||||||
|
"team.update": ProcessTeamUpdate,
|
||||||
|
"team.delete": ProcessTeamDelete,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Attach attaches the signin handlers to the router
|
// Attach attaches the signin handlers to the router
|
||||||
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
|
|
||||||
|
|
@ -43,11 +55,11 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
team.Use(oauth.Guard)
|
team.Use(oauth.Guard)
|
||||||
|
|
||||||
// Team CRUD
|
// Team CRUD
|
||||||
team.GET("/", placeholder) // Get user teams
|
team.GET("/", GinTeamList) // Get user teams
|
||||||
team.GET("/:team_id", placeholder) // Get user team details
|
team.GET("/:team_id", GinTeamGet) // Get user team details
|
||||||
team.POST("/", placeholder) // Create user team
|
team.POST("/", GinTeamCreate) // Create user team
|
||||||
team.PUT("/:team_id", placeholder) // Update user team
|
team.PUT("/:team_id", GinTeamUpdate) // Update user team
|
||||||
team.DELETE("/:team_id", placeholder) // Delete user team
|
team.DELETE("/:team_id", GinTeamDelete) // Delete user team
|
||||||
|
|
||||||
// Member Management
|
// Member Management
|
||||||
team.GET("/:team_id/members", placeholder) // Get user team members
|
team.GET("/:team_id/members", placeholder) // Get user team members
|
||||||
|
|
|
||||||
149
openapi/user/utils.go
Normal file
149
openapi/user/utils.go
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/process"
|
||||||
|
"github.com/yaoapp/gou/session"
|
||||||
|
"github.com/yaoapp/kun/exception"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Session Utilities
|
||||||
|
|
||||||
|
// GetUserIDFromSession gets the current user ID from session
|
||||||
|
// Returns the user ID string or throws an exception if not authenticated
|
||||||
|
func GetUserIDFromSession(process *process.Process) string {
|
||||||
|
sessionData, err := session.Global().ID(process.Sid).Get("__user_id")
|
||||||
|
if err != nil || sessionData == nil {
|
||||||
|
exception.New("user not authenticated", 401).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDStr, ok := sessionData.(string)
|
||||||
|
if !ok {
|
||||||
|
exception.New("invalid user_id in session", 401).Throw()
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIDStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type Conversion Utilities
|
||||||
|
|
||||||
|
// toBool converts various types to boolean
|
||||||
|
// Supports: bool, int, int64, float64, string
|
||||||
|
// Returns false for nil or unsupported types
|
||||||
|
func toBool(v interface{}) bool {
|
||||||
|
if v == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return val
|
||||||
|
case int:
|
||||||
|
return val != 0
|
||||||
|
case int64:
|
||||||
|
return val != 0
|
||||||
|
case float64:
|
||||||
|
return val != 0
|
||||||
|
case string:
|
||||||
|
return val == "true" || val == "1"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toString converts various types to string
|
||||||
|
// Supports: string, int, int64, float64, bool
|
||||||
|
// Returns empty string for nil or unsupported types
|
||||||
|
func toString(v interface{}) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
return val
|
||||||
|
case int:
|
||||||
|
return fmt.Sprintf("%d", val)
|
||||||
|
case int64:
|
||||||
|
return fmt.Sprintf("%d", val)
|
||||||
|
case float64:
|
||||||
|
return fmt.Sprintf("%.0f", val)
|
||||||
|
case bool:
|
||||||
|
if val {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toInt64 converts various types to int64
|
||||||
|
// Supports: int, int64, float64, string
|
||||||
|
// Returns 0 for nil or unsupported types
|
||||||
|
func toInt64(v interface{}) int64 {
|
||||||
|
if v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case int64:
|
||||||
|
return val
|
||||||
|
case int:
|
||||||
|
return int64(val)
|
||||||
|
case float64:
|
||||||
|
return int64(val)
|
||||||
|
case string:
|
||||||
|
if parsed, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toTimeString converts various time types to RFC3339 string
|
||||||
|
// Supports: time.Time, string, int64 (unix timestamp)
|
||||||
|
// Returns empty string for nil or unsupported types
|
||||||
|
func toTimeString(v interface{}) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case time.Time:
|
||||||
|
if val.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.Format(time.RFC3339)
|
||||||
|
case string:
|
||||||
|
// Try to parse as RFC3339 first
|
||||||
|
if t, err := time.Parse(time.RFC3339, val); err == nil {
|
||||||
|
return t.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
// Try to parse as other common formats
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
"2006-01-02T15:04:05Z",
|
||||||
|
"2006-01-02T15:04:05.000Z",
|
||||||
|
}
|
||||||
|
for _, format := range formats {
|
||||||
|
if t, err := time.Parse(format, val); err == nil {
|
||||||
|
return t.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val // Return as-is if can't parse
|
||||||
|
case int64:
|
||||||
|
// Assume unix timestamp
|
||||||
|
if val > 0 {
|
||||||
|
return time.Unix(val, 0).Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue