Add Signin API and update file management endpoints
- Introduced a comprehensive Signin API for user authentication, supporting multiple OAuth providers (Google, GitHub, Microsoft, Apple). - Updated the file management API endpoints to use a singular `/file` path instead of `/files`, enhancing consistency across the API. - Revised README documentation to include detailed descriptions of the new Signin API and updated file management endpoints. - Enhanced test cases to reflect the changes in endpoint structure and ensure robust coverage for the new Signin functionality.
This commit is contained in:
parent
1f2eaf6974
commit
315618da47
10 changed files with 932 additions and 80 deletions
24
.github/workflows/pr-test.yml
vendored
24
.github/workflows/pr-test.yml
vendored
|
|
@ -80,6 +80,25 @@ env:
|
|||
S3_BUCKET: ${{ secrets.S3_BUCKET }}
|
||||
S3_PUBLIC_URL: ${{ secrets.S3_PUBLIC_URL }}
|
||||
|
||||
# === Openapi Signin Configs ===
|
||||
## Google
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
|
||||
## Microsoft
|
||||
MICROSOFT_CLIENT_ID: ${{ secrets.MICROSOFT_CLIENT_ID }}
|
||||
MICROSOFT_CLIENT_SECRET: ${{ secrets.MICROSOFT_CLIENT_SECRET }}
|
||||
|
||||
## Apple
|
||||
APPLE_SERVICE_ID: ${{ secrets.APPLE_SERVICE_ID }}
|
||||
APPLE_PRIVATE_KEY_PATH: "apple/signin_client_secret_key.p8"
|
||||
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
## Github
|
||||
GITHUBUSER_CLIENT_ID: ${{ secrets.GITHUBUSER_CLIENT_ID }}
|
||||
GITHUBUSER_CLIENT_SECRET: ${{ secrets.GITHUBUSER_CLIENT_SECRET }}
|
||||
|
||||
jobs:
|
||||
UnitTest:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -236,6 +255,11 @@ jobs:
|
|||
with:
|
||||
ref: ${{ env.HEAD }}
|
||||
|
||||
- name: Setup Apple Private Key
|
||||
run: |
|
||||
mkdir -p ../app/openapi/certs/apple
|
||||
echo "${{ secrets.APPLE_PRIVATE_KEY_USER }}" > ../app/openapi/certs/apple/signin_client_secret_key.p8
|
||||
|
||||
- name: Start Redis
|
||||
uses: supercharge/redis-github-action@1.4.0
|
||||
with:
|
||||
|
|
|
|||
25
.github/workflows/unit-test.yml
vendored
25
.github/workflows/unit-test.yml
vendored
|
|
@ -84,6 +84,26 @@ env:
|
|||
S3_BUCKET: ${{ secrets.S3_BUCKET }}
|
||||
S3_PUBLIC_URL: ${{ secrets.S3_PUBLIC_URL }}
|
||||
|
||||
|
||||
# === Openapi Signin Configs ===
|
||||
## Google
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
|
||||
## Microsoft
|
||||
MICROSOFT_CLIENT_ID: ${{ secrets.MICROSOFT_CLIENT_ID }}
|
||||
MICROSOFT_CLIENT_SECRET: ${{ secrets.MICROSOFT_CLIENT_SECRET }}
|
||||
|
||||
## Apple
|
||||
APPLE_SERVICE_ID: ${{ secrets.APPLE_SERVICE_ID }}
|
||||
APPLE_PRIVATE_KEY_PATH: "apple/signin_client_secret_key.p8"
|
||||
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
## Github
|
||||
GITHUBUSER_CLIENT_ID: ${{ secrets.GITHUBUSER_CLIENT_ID }}
|
||||
GITHUBUSER_CLIENT_SECRET: ${{ secrets.GITHUBUSER_CLIENT_SECRET }}
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -190,6 +210,11 @@ jobs:
|
|||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Apple Private Key
|
||||
run: |
|
||||
mkdir -p ../app/openapi/certs/apple
|
||||
echo "${{ secrets.APPLE_PRIVATE_KEY_USER }}" > ../app/openapi/certs/apple/signin_client_secret_key.p8
|
||||
|
||||
- name: Setup Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -322,6 +322,140 @@ openai.api_key = "your-oauth-token"
|
|||
|
||||
All Chat endpoints require OAuth authentication.
|
||||
|
||||
## Signin API
|
||||
|
||||
Comprehensive authentication API for user signin, configuration management, and OAuth integration with support for multiple authentication providers.
|
||||
|
||||
The Signin API provides:
|
||||
|
||||
- **Signin Configuration**: Get public signin configuration for different locales
|
||||
- **Password Authentication**: Traditional username/password signin flow
|
||||
- **OAuth Integration**: Third-party authentication provider callbacks
|
||||
- **Multi-Locale Support**: Localized signin configurations and messages
|
||||
- **Provider Management**: Support for multiple OAuth providers (Google, GitHub, etc.)
|
||||
|
||||
**Key Endpoints:**
|
||||
|
||||
- `GET /signin` - Get signin configuration for locale
|
||||
- `POST /signin` - Authenticate with username/password
|
||||
- `GET /signin/authback/{id}` - OAuth authentication callback handler
|
||||
|
||||
**Configuration:**
|
||||
|
||||
Signin configurations are defined in DSL files with multi-locale support:
|
||||
|
||||
**[View Configuration Examples →](https://github.com/YaoApp/yao-dev-app/blob/main/openapi/signin.en.yao)**
|
||||
|
||||
### Get Signin Configuration
|
||||
|
||||
Retrieve public signin configuration for a specific locale:
|
||||
|
||||
```
|
||||
GET /signin?locale={locale}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `locale` (optional): Language locale (e.g., "en", "zh-cn")
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/signin?locale=en" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Sign In",
|
||||
"subtitle": "Welcome back",
|
||||
"providers": [
|
||||
{
|
||||
"id": "google",
|
||||
"name": "Google",
|
||||
"icon": "google",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "github",
|
||||
"name": "GitHub",
|
||||
"icon": "github",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"password_enabled": true,
|
||||
"register_enabled": true,
|
||||
"forgot_password_enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### Password Signin
|
||||
|
||||
Authenticate using username and password:
|
||||
|
||||
```
|
||||
POST /signin
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "user@example.com",
|
||||
"password": "your_password",
|
||||
"remember": true
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJSUzI1NiIs...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "def50200...",
|
||||
"user": {
|
||||
"id": "user123",
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth Authentication Callback
|
||||
|
||||
Handle OAuth provider authentication callbacks:
|
||||
|
||||
```
|
||||
GET /signin/authback/{provider_id}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `provider_id` (path): OAuth provider identifier (e.g., "google", "github")
|
||||
- Standard OAuth parameters in query string (code, state, etc.)
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
GET /signin/authback/google?code=auth_code&state=csrf_token
|
||||
```
|
||||
|
||||
This endpoint processes the OAuth callback and returns authentication tokens or redirects to the configured success/error URLs.
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Multi-Provider Support**: Google, GitHub, Microsoft, and custom OAuth providers
|
||||
- **Locale Awareness**: Configuration adapts to user's preferred language
|
||||
- **Security**: CSRF protection, secure token handling, and validation
|
||||
- **Customizable UI**: Configurable signin forms and provider buttons
|
||||
- **Session Management**: Automatic session creation and token management
|
||||
|
||||
**Note:** Signin endpoints are publicly accessible for authentication purposes, but return OAuth tokens that must be used for subsequent API calls.
|
||||
|
||||
## File Management API
|
||||
|
||||
Comprehensive API for managing file uploads, downloads, and file operations with support for multiple storage backends.
|
||||
|
|
@ -340,12 +474,12 @@ The File Management API provides:
|
|||
|
||||
**Key Endpoints:**
|
||||
|
||||
- `POST /files/{uploaderID}` - Upload files (supports chunked upload)
|
||||
- `GET /files/{uploaderID}` - List files with pagination and filters
|
||||
- `GET /files/{uploaderID}/{fileID}` - Get file metadata
|
||||
- `GET /files/{uploaderID}/{fileID}/content` - Download file content
|
||||
- `GET /files/{uploaderID}/{fileID}/exists` - Check file existence
|
||||
- `DELETE /files/{uploaderID}/{fileID}` - Delete file
|
||||
- `POST /file/{uploaderID}` - Upload files (supports chunked upload)
|
||||
- `GET /file/{uploaderID}` - List files with pagination and filters
|
||||
- `GET /file/{uploaderID}/{fileID}` - Get file metadata
|
||||
- `GET /file/{uploaderID}/{fileID}/content` - Download file content
|
||||
- `GET /file/{uploaderID}/{fileID}/exists` - Check file existence
|
||||
- `DELETE /file/{uploaderID}/{fileID}` - Delete file
|
||||
|
||||
**Advanced Features:**
|
||||
|
||||
|
|
@ -519,12 +653,40 @@ curl -X GET "/v1/chat/completions?content=Help%20me%20create%20a%20user%20model&
|
|||
-H "Accept: text/event-stream"
|
||||
```
|
||||
|
||||
### User Authentication with Signin API
|
||||
|
||||
1. **Get signin configuration**:
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/signin?locale=en" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
2. **Authenticate with password**:
|
||||
|
||||
```bash
|
||||
curl -X POST "/v1/signin" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "user@example.com",
|
||||
"password": "secure_password",
|
||||
"remember": true
|
||||
}'
|
||||
```
|
||||
|
||||
3. **Use authentication token for API access**:
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/dsl/list/model" \
|
||||
-H "Authorization: Bearer {received_access_token}"
|
||||
```
|
||||
|
||||
### File Upload and Management
|
||||
|
||||
1. **Upload a file with metadata**:
|
||||
|
||||
```bash
|
||||
curl -X POST "/v1/files/default" \
|
||||
curl -X POST "/v1/file/default" \
|
||||
-H "Authorization: Bearer {access_token}" \
|
||||
-F "file=@document.pdf" \
|
||||
-F "path=documents/reports/quarterly-report.pdf" \
|
||||
|
|
@ -536,14 +698,14 @@ curl -X POST "/v1/files/default" \
|
|||
2. **List and filter files**:
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/files/default?status=completed&content_type=application/pdf&page=1&page_size=10" \
|
||||
curl -X GET "/v1/file/default?status=completed&content_type=application/pdf&page=1&page_size=10" \
|
||||
-H "Authorization: Bearer {access_token}"
|
||||
```
|
||||
|
||||
3. **Download file content** (with optimized delivery):
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/files/default/{file_id}/content" \
|
||||
curl -X GET "/v1/file/default/{file_id}/content" \
|
||||
-H "Authorization: Bearer {access_token}" \
|
||||
--output downloaded-document.pdf
|
||||
```
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ This document describes the RESTful API for managing file uploads, downloads, an
|
|||
|
||||
## Base URL
|
||||
|
||||
All endpoints are prefixed with the configured base URL followed by `/files` (e.g., `/v1/files`).
|
||||
All endpoints are prefixed with the configured base URL followed by `/file` (e.g., `/v1/file`).
|
||||
|
||||
## Authentication
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ The File Management API provides comprehensive file handling capabilities includ
|
|||
Upload files with support for chunked uploads, compression, and metadata.
|
||||
|
||||
```
|
||||
POST /files/{uploaderID}
|
||||
POST /file/{uploaderID}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
|
@ -57,7 +57,7 @@ POST /files/{uploaderID}
|
|||
|
||||
```bash
|
||||
# Simple file upload
|
||||
curl -X POST "/v1/files/default" \
|
||||
curl -X POST "/v1/file/default" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-F "file=@document.pdf" \
|
||||
-F "path=documents/reports/quarterly-report.pdf" \
|
||||
|
|
@ -66,7 +66,7 @@ curl -X POST "/v1/files/default" \
|
|||
-F "gzip=true"
|
||||
|
||||
# Chunked upload (first chunk)
|
||||
curl -X POST "/v1/files/default" \
|
||||
curl -X POST "/v1/file/default" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Range: bytes 0-1023/2048" \
|
||||
-H "Content-Sync: chunk-upload" \
|
||||
|
|
@ -95,7 +95,7 @@ curl -X POST "/v1/files/default" \
|
|||
List files with pagination, filtering, and sorting capabilities.
|
||||
|
||||
```
|
||||
GET /files/{uploaderID}?page={page}&page_size={page_size}&status={status}&content_type={content_type}&name={name}&order_by={order_by}&select={select}
|
||||
GET /file/{uploaderID}?page={page}&page_size={page_size}&status={status}&content_type={content_type}&name={name}&order_by={order_by}&select={select}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
|
@ -116,15 +116,15 @@ GET /files/{uploaderID}?page={page}&page_size={page_size}&status={status}&conten
|
|||
|
||||
```bash
|
||||
# List files with pagination
|
||||
curl -X GET "/v1/files/default?page=1&page_size=10" \
|
||||
curl -X GET "/v1/file/default?page=1&page_size=10" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
|
||||
# List files with filters
|
||||
curl -X GET "/v1/files/default?status=completed&content_type=image/jpeg&name=photo*" \
|
||||
curl -X GET "/v1/file/default?status=completed&content_type=image/jpeg&name=photo*" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
|
||||
# List with custom ordering and field selection
|
||||
curl -X GET "/v1/files/default?order_by=bytes desc&select=file_id,filename,bytes" \
|
||||
curl -X GET "/v1/file/default?order_by=bytes desc&select=file_id,filename,bytes" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ curl -X GET "/v1/files/default?order_by=bytes desc&select=file_id,filename,bytes
|
|||
Get detailed metadata for a specific file.
|
||||
|
||||
```
|
||||
GET /files/{uploaderID}/{fileID}
|
||||
GET /file/{uploaderID}/{fileID}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
|
@ -168,7 +168,7 @@ GET /files/{uploaderID}/{fileID}
|
|||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/files/default/a1b2c3d4e5f6789012345678901234567890abcd" \
|
||||
curl -X GET "/v1/file/default/a1b2c3d4e5f6789012345678901234567890abcd" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ curl -X GET "/v1/files/default/a1b2c3d4e5f6789012345678901234567890abcd" \
|
|||
Download the actual file content directly from storage.
|
||||
|
||||
```
|
||||
GET /files/{uploaderID}/{fileID}/content
|
||||
GET /file/{uploaderID}/{fileID}/content
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
|
@ -208,7 +208,7 @@ GET /files/{uploaderID}/{fileID}/content
|
|||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/files/default/a1b2c3d4e5f6789012345678901234567890abcd/content" \
|
||||
curl -X GET "/v1/file/default/a1b2c3d4e5f6789012345678901234567890abcd/content" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
--output downloaded-file.pdf
|
||||
```
|
||||
|
|
@ -235,7 +235,7 @@ Content-Length: 2048576
|
|||
Check if a file exists without downloading it.
|
||||
|
||||
```
|
||||
GET /files/{uploaderID}/{fileID}/exists
|
||||
GET /file/{uploaderID}/{fileID}/exists
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
|
@ -246,7 +246,7 @@ GET /files/{uploaderID}/{fileID}/exists
|
|||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/files/default/a1b2c3d4e5f6789012345678901234567890abcd/exists" \
|
||||
curl -X GET "/v1/file/default/a1b2c3d4e5f6789012345678901234567890abcd/exists" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ curl -X GET "/v1/files/default/a1b2c3d4e5f6789012345678901234567890abcd/exists"
|
|||
Delete a file and its metadata.
|
||||
|
||||
```
|
||||
DELETE /files/{uploaderID}/{fileID}
|
||||
DELETE /file/{uploaderID}/{fileID}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
|
@ -275,7 +275,7 @@ DELETE /files/{uploaderID}/{fileID}
|
|||
**Example:**
|
||||
|
||||
```bash
|
||||
curl -X DELETE "/v1/files/default/a1b2c3d4e5f6789012345678901234567890abcd" \
|
||||
curl -X DELETE "/v1/file/default/a1b2c3d4e5f6789012345678901234567890abcd" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
|
|
@ -340,7 +340,7 @@ For large files, use chunked upload for better reliability:
|
|||
|
||||
```bash
|
||||
# Upload chunk 1
|
||||
curl -X POST "/v1/files/default" \
|
||||
curl -X POST "/v1/file/default" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Range: bytes 0-1048575/3145728" \
|
||||
-H "Content-Sync: chunk-upload" \
|
||||
|
|
@ -348,7 +348,7 @@ curl -X POST "/v1/files/default" \
|
|||
-F "file=@chunk1.bin"
|
||||
|
||||
# Upload chunk 2
|
||||
curl -X POST "/v1/files/default" \
|
||||
curl -X POST "/v1/file/default" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Range: bytes 1048576-2097151/3145728" \
|
||||
-H "Content-Sync: chunk-upload" \
|
||||
|
|
@ -356,7 +356,7 @@ curl -X POST "/v1/files/default" \
|
|||
-F "file=@chunk2.bin"
|
||||
|
||||
# Upload final chunk (triggers merge)
|
||||
curl -X POST "/v1/files/default" \
|
||||
curl -X POST "/v1/file/default" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Range: bytes 2097152-3145727/3145728" \
|
||||
-H "Content-Sync: chunk-upload" \
|
||||
|
|
@ -397,7 +397,7 @@ All endpoints return standardized error responses:
|
|||
1. **Upload a file:**
|
||||
|
||||
```bash
|
||||
curl -X POST "/v1/files/default" \
|
||||
curl -X POST "/v1/file/default" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-F "file=@document.pdf" \
|
||||
-F "path=documents/important-doc.pdf" \
|
||||
|
|
@ -407,14 +407,14 @@ curl -X POST "/v1/files/default" \
|
|||
2. **List files to find the uploaded file:**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/files/default?name=important-doc*" \
|
||||
curl -X GET "/v1/file/default?name=important-doc*" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
3. **Download the file:**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/files/default/{file_id}/content" \
|
||||
curl -X GET "/v1/file/default/{file_id}/content" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
--output downloaded-document.pdf
|
||||
```
|
||||
|
|
@ -439,7 +439,7 @@ for i in chunk_*; do
|
|||
START=$((CHUNK_SIZE * (${i#chunk_} - 1)))
|
||||
END=$((START + $(stat -c%s $i) - 1))
|
||||
|
||||
curl -X POST "/v1/files/default" \
|
||||
curl -X POST "/v1/file/default" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Range: bytes ${START}-${END}/${TOTAL_SIZE}" \
|
||||
-H "Content-Sync: chunk-upload" \
|
||||
|
|
@ -453,7 +453,7 @@ done
|
|||
1. **Upload with comprehensive metadata:**
|
||||
|
||||
```bash
|
||||
curl -X POST "/v1/files/default" \
|
||||
curl -X POST "/v1/file/default" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-F "file=@report.pdf" \
|
||||
-F "path=reports/2024/quarterly-report.pdf" \
|
||||
|
|
@ -466,21 +466,21 @@ curl -X POST "/v1/files/default" \
|
|||
2. **List files with filters:**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/files/default?status=completed&content_type=application/pdf&order_by=created_at desc" \
|
||||
curl -X GET "/v1/file/default?status=completed&content_type=application/pdf&order_by=created_at desc" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
3. **Get detailed file information:**
|
||||
|
||||
```bash
|
||||
curl -X GET "/v1/files/default/{file_id}" \
|
||||
curl -X GET "/v1/file/default/{file_id}" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
4. **Clean up old files:**
|
||||
|
||||
```bash
|
||||
curl -X DELETE "/v1/files/default/{file_id}" \
|
||||
curl -X DELETE "/v1/file/default/{file_id}" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -20,22 +20,22 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
group.Use(oauth.Guard)
|
||||
|
||||
// Upload a file (supports chunked upload)
|
||||
group.POST("/files/:uploaderID", upload)
|
||||
group.POST("/:uploaderID", upload)
|
||||
|
||||
// List files
|
||||
group.GET("/files/:uploaderID", list)
|
||||
group.GET("/:uploaderID", list)
|
||||
|
||||
// Retrieve file
|
||||
group.GET("/files/:uploaderID/:fileID", retrieve)
|
||||
group.GET("/:uploaderID/:fileID", retrieve)
|
||||
|
||||
// Delete file
|
||||
group.DELETE("/files/:uploaderID/:fileID", delete)
|
||||
group.DELETE("/:uploaderID/:fileID", delete)
|
||||
|
||||
// Retrieve file content
|
||||
group.GET("/files/:uploaderID/:fileID/content", content)
|
||||
group.GET("/:uploaderID/:fileID/content", content)
|
||||
|
||||
// Check if file exists
|
||||
group.GET("/files/:uploaderID/:fileID/exists", exists)
|
||||
group.GET("/:uploaderID/:fileID/exists", exists)
|
||||
}
|
||||
|
||||
// upload handles file upload
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/yaoapp/yao/openapi/kb"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/signin"
|
||||
)
|
||||
|
||||
// Server is the OpenAPI server
|
||||
|
|
@ -52,6 +53,12 @@ func Load(appConfig config.Config) (*OpenAPI, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Load signin configurations
|
||||
err = signin.Load(appConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the OpenAPI server
|
||||
Server = &OpenAPI{Config: &config, OAuth: oauthService}
|
||||
return Server, nil
|
||||
|
|
@ -82,7 +89,7 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) {
|
|||
dsl.Attach(group.Group("/dsl"), openapi.OAuth)
|
||||
|
||||
// File handlers
|
||||
file.Attach(group, openapi.OAuth)
|
||||
file.Attach(group.Group("/file"), openapi.OAuth)
|
||||
|
||||
// Knowledge Base handlers
|
||||
kb.Attach(group.Group("/kb"), openapi.OAuth)
|
||||
|
|
@ -90,5 +97,8 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) {
|
|||
// Chat handlers
|
||||
chat.Attach(group.Group("/chat"), openapi.OAuth)
|
||||
|
||||
// Signin handlers
|
||||
signin.Attach(group, openapi.OAuth)
|
||||
|
||||
// Custom handlers (Defined by developer)
|
||||
}
|
||||
|
|
|
|||
42
openapi/signin/api.go
Normal file
42
openapi/signin/api.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package signin
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
// Attach attaches the signin handlers to the router
|
||||
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||
group.GET("/signin", getConfig)
|
||||
group.POST("/signin", signin)
|
||||
group.GET("/signin/authback/:id", authback)
|
||||
}
|
||||
|
||||
// getConfig is the handler for get signin configuration
|
||||
func getConfig(c *gin.Context) {
|
||||
// Get locale from query parameter (optional)
|
||||
locale := c.Query("locale")
|
||||
|
||||
// Get public configuration for the specified locale
|
||||
config := GetPublicConfig(locale)
|
||||
|
||||
// If no configuration found, return error
|
||||
if config == nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "No signin configuration found for the requested locale",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Return the public configuration
|
||||
response.RespondWithSuccess(c, response.StatusOK, config)
|
||||
}
|
||||
|
||||
// signin is the handler for signin (password login)
|
||||
func signin(c *gin.Context) {}
|
||||
|
||||
// authback is the handler for authback
|
||||
func authback(c *gin.Context) {}
|
||||
388
openapi/signin/signin.go
Normal file
388
openapi/signin/signin.go
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
package signin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/yaoapp/gou/application"
|
||||
"github.com/yaoapp/yao/config"
|
||||
)
|
||||
|
||||
// Global variables to store loaded configurations
|
||||
var (
|
||||
// Full configurations with sensitive data (for backend use)
|
||||
fullConfigs = make(map[string]*Config)
|
||||
// Public configurations without sensitive data (for frontend use)
|
||||
publicConfigs = make(map[string]*Config)
|
||||
// Default language code
|
||||
defaultLang = ""
|
||||
// Mutex for thread safety
|
||||
configMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// Config represents the signin page configuration
|
||||
type Config struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SuccessURL string `json:"success_url,omitempty"`
|
||||
FailureURL string `json:"failure_url,omitempty"`
|
||||
Form *FormConfig `json:"form,omitempty"`
|
||||
Token *TokenConfig `json:"token,omitempty"`
|
||||
ThirdParty *ThirdParty `json:"third_party,omitempty"`
|
||||
}
|
||||
|
||||
// FormConfig represents the form configuration
|
||||
type FormConfig struct {
|
||||
Username *UsernameConfig `json:"username,omitempty"`
|
||||
Password *PasswordConfig `json:"password,omitempty"`
|
||||
Captcha *CaptchaConfig `json:"captcha,omitempty"`
|
||||
ForgotPasswordLink bool `json:"forgot_password_link,omitempty"`
|
||||
RememberMe bool `json:"remember_me,omitempty"`
|
||||
RegisterLink string `json:"register_link,omitempty"`
|
||||
TermsOfServiceLink string `json:"terms_of_service_link,omitempty"`
|
||||
PrivacyPolicyLink string `json:"privacy_policy_link,omitempty"`
|
||||
}
|
||||
|
||||
// UsernameConfig represents the username field configuration
|
||||
type UsernameConfig struct {
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Fields []string `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// PasswordConfig represents the password field configuration
|
||||
type PasswordConfig struct {
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
}
|
||||
|
||||
// CaptchaConfig represents the captcha configuration
|
||||
type CaptchaConfig struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Options map[string]interface{} `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// TokenConfig represents the token configuration
|
||||
type TokenConfig struct {
|
||||
ExpiresIn string `json:"expires_in,omitempty"`
|
||||
RememberMeExpiresIn string `json:"remember_me_expires_in,omitempty"`
|
||||
}
|
||||
|
||||
// ThirdParty represents the third party login configuration
|
||||
type ThirdParty struct {
|
||||
Register *RegisterConfig `json:"register,omitempty"`
|
||||
Providers []*Provider `json:"providers,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterConfig represents the auto register configuration
|
||||
type RegisterConfig struct {
|
||||
Auto bool `json:"auto,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}
|
||||
|
||||
// Provider represents a third party login provider
|
||||
type Provider struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Logo string `json:"logo,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
ClientSecretGenerator *SecretGenerator `json:"client_secret_generator,omitempty"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
Endpoints *Endpoints `json:"endpoints,omitempty"`
|
||||
Mapping map[string]string `json:"mapping,omitempty"`
|
||||
}
|
||||
|
||||
// SecretGenerator represents the client secret generator configuration
|
||||
type SecretGenerator struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
ExpiresIn string `json:"expires_in,omitempty"`
|
||||
PrivateKey string `json:"private_key,omitempty"`
|
||||
Header map[string]interface{} `json:"header,omitempty"`
|
||||
Payload map[string]interface{} `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// Endpoints represents the OAuth endpoints
|
||||
type Endpoints struct {
|
||||
Authorization string `json:"authorization,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
UserInfo string `json:"user_info,omitempty"`
|
||||
}
|
||||
|
||||
// Load loads all signin configurations from the openapi directory
|
||||
func Load(appConfig config.Config) error {
|
||||
configMutex.Lock()
|
||||
defer configMutex.Unlock()
|
||||
|
||||
// Clear existing configurations
|
||||
fullConfigs = make(map[string]*Config)
|
||||
publicConfigs = make(map[string]*Config)
|
||||
defaultLang = ""
|
||||
|
||||
// Find all signin configuration files
|
||||
files, err := findSigninFiles()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find signin files: %v", err)
|
||||
}
|
||||
|
||||
// If no signin files found, that's not necessarily an error
|
||||
// Some applications might not have signin configurations
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load each configuration file
|
||||
for _, file := range files {
|
||||
lang := extractLanguageFromFilename(file)
|
||||
|
||||
configPath := filepath.Join("openapi", file)
|
||||
configRaw, err := application.App.Read(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read signin config %s: %v", file, err)
|
||||
}
|
||||
|
||||
// Parse the configuration
|
||||
var signinConfig Config
|
||||
err = application.Parse(configPath, configRaw, &signinConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse signin config %s: %v", file, err)
|
||||
}
|
||||
|
||||
// Process ENV variables in full config
|
||||
fullConfig := signinConfig
|
||||
processENVVariables(&fullConfig, appConfig.Root)
|
||||
|
||||
// Create public config (without sensitive data)
|
||||
publicConfig := createPublicConfig(&fullConfig)
|
||||
|
||||
// Store configurations
|
||||
fullConfigs[lang] = &fullConfig
|
||||
publicConfigs[lang] = &publicConfig
|
||||
|
||||
// Set default language
|
||||
if defaultLang == "" || lang == "en" || file == "signin.yao" {
|
||||
defaultLang = lang
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findSigninFiles finds all signin configuration files in the openapi directory
|
||||
func findSigninFiles() ([]string, error) {
|
||||
var files []string
|
||||
signinFilePattern := regexp.MustCompile(`^signin(\.[a-z]{2}(-[a-z]{2})?)?\.yao$`)
|
||||
|
||||
// Use Walk to find all signin files in the openapi directory
|
||||
err := application.App.Walk("openapi", func(root, filename string, isdir bool) error {
|
||||
if isdir {
|
||||
return nil
|
||||
}
|
||||
|
||||
baseName := filepath.Base(filename)
|
||||
if signinFilePattern.MatchString(baseName) {
|
||||
files = append(files, baseName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, "*.yao")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// extractLanguageFromFilename extracts language code from filename
|
||||
func extractLanguageFromFilename(filename string) string {
|
||||
// signin.yao -> ""
|
||||
// signin.en.yao -> "en"
|
||||
// signin.zh-cn.yao -> "zh-cn"
|
||||
|
||||
if filename == "signin.yao" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(filename, ".")
|
||||
if len(parts) >= 3 {
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// processENVVariables processes environment variables in the configuration
|
||||
func processENVVariables(config *Config, rootPath string) {
|
||||
// Process form captcha options
|
||||
if config.Form != nil && config.Form.Captcha != nil && config.Form.Captcha.Options != nil {
|
||||
for key, value := range config.Form.Captcha.Options {
|
||||
if strValue, ok := value.(string); ok {
|
||||
config.Form.Captcha.Options[key] = replaceENVVar(strValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process third party providers
|
||||
if config.ThirdParty != nil && config.ThirdParty.Providers != nil {
|
||||
for _, provider := range config.ThirdParty.Providers {
|
||||
provider.ClientID = replaceENVVar(provider.ClientID)
|
||||
provider.ClientSecret = replaceENVVar(provider.ClientSecret)
|
||||
|
||||
// Process client secret generator
|
||||
if provider.ClientSecretGenerator != nil {
|
||||
provider.ClientSecretGenerator.PrivateKey = replaceENVVar(provider.ClientSecretGenerator.PrivateKey)
|
||||
|
||||
// Convert relative path to absolute path for private key
|
||||
if provider.ClientSecretGenerator.PrivateKey != "" && !filepath.IsAbs(provider.ClientSecretGenerator.PrivateKey) {
|
||||
provider.ClientSecretGenerator.PrivateKey = filepath.Join(rootPath, "openapi", "certs", provider.ClientSecretGenerator.PrivateKey)
|
||||
}
|
||||
|
||||
// Process header values
|
||||
if provider.ClientSecretGenerator.Header != nil {
|
||||
for key, value := range provider.ClientSecretGenerator.Header {
|
||||
if strValue, ok := value.(string); ok {
|
||||
provider.ClientSecretGenerator.Header[key] = replaceENVVar(strValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process payload values
|
||||
if provider.ClientSecretGenerator.Payload != nil {
|
||||
for key, value := range provider.ClientSecretGenerator.Payload {
|
||||
if strValue, ok := value.(string); ok {
|
||||
provider.ClientSecretGenerator.Payload[key] = replaceENVVar(strValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replaceENVVar replaces environment variables in the format $ENV.VAR_NAME
|
||||
func replaceENVVar(value string) string {
|
||||
if strings.HasPrefix(value, "$ENV.") {
|
||||
envVar := strings.TrimPrefix(value, "$ENV.")
|
||||
if envValue := os.Getenv(envVar); envValue != "" {
|
||||
return envValue
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// createPublicConfig creates a public version of the configuration without sensitive data
|
||||
func createPublicConfig(fullConfig *Config) Config {
|
||||
publicConfig := *fullConfig
|
||||
|
||||
// Remove sensitive data from third party providers
|
||||
if publicConfig.ThirdParty != nil && publicConfig.ThirdParty.Providers != nil {
|
||||
publicProviders := make([]*Provider, len(publicConfig.ThirdParty.Providers))
|
||||
for i, provider := range publicConfig.ThirdParty.Providers {
|
||||
publicProvider := *provider
|
||||
|
||||
// Remove sensitive fields
|
||||
publicProvider.ClientSecret = ""
|
||||
publicProvider.ClientSecretGenerator = nil
|
||||
|
||||
publicProviders[i] = &publicProvider
|
||||
}
|
||||
publicConfig.ThirdParty.Providers = publicProviders
|
||||
}
|
||||
|
||||
return publicConfig
|
||||
}
|
||||
|
||||
// GetFullConfig returns the full configuration for a given language
|
||||
func GetFullConfig(lang string) *Config {
|
||||
configMutex.RLock()
|
||||
defer configMutex.RUnlock()
|
||||
|
||||
// Normalize language code to lowercase
|
||||
if lang != "" {
|
||||
lang = strings.ToLower(lang)
|
||||
}
|
||||
|
||||
// Try to get specific language config
|
||||
if config, exists := fullConfigs[lang]; exists {
|
||||
return config
|
||||
}
|
||||
|
||||
// Fallback to default language
|
||||
if defaultLang != "" {
|
||||
if config, exists := fullConfigs[defaultLang]; exists {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
// Return any available config as last resort
|
||||
for _, config := range fullConfigs {
|
||||
return config
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPublicConfig returns the public configuration for a given language
|
||||
func GetPublicConfig(lang string) *Config {
|
||||
configMutex.RLock()
|
||||
defer configMutex.RUnlock()
|
||||
|
||||
// Normalize language code to lowercase
|
||||
if lang != "" {
|
||||
lang = strings.ToLower(lang)
|
||||
}
|
||||
|
||||
// Try to get specific language config
|
||||
if config, exists := publicConfigs[lang]; exists {
|
||||
return config
|
||||
}
|
||||
|
||||
// Fallback to default language
|
||||
if defaultLang != "" {
|
||||
if config, exists := publicConfigs[defaultLang]; exists {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
// Return any available config as last resort
|
||||
for _, config := range publicConfigs {
|
||||
return config
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAvailableLanguages returns all available language codes
|
||||
func GetAvailableLanguages() []string {
|
||||
configMutex.RLock()
|
||||
defer configMutex.RUnlock()
|
||||
|
||||
var languages []string
|
||||
for lang := range fullConfigs {
|
||||
if lang != "" {
|
||||
languages = append(languages, lang)
|
||||
}
|
||||
}
|
||||
|
||||
// Add default language if it exists and is empty string
|
||||
if defaultLang == "" && len(fullConfigs) > 0 {
|
||||
languages = append(languages, "default")
|
||||
}
|
||||
|
||||
return languages
|
||||
}
|
||||
|
||||
// GetDefaultLanguage returns the default language code
|
||||
func GetDefaultLanguage() string {
|
||||
configMutex.RLock()
|
||||
defer configMutex.RUnlock()
|
||||
|
||||
if defaultLang == "" {
|
||||
return "default"
|
||||
}
|
||||
return defaultLang
|
||||
}
|
||||
|
|
@ -127,7 +127,7 @@ func TestFileUpload(t *testing.T) {
|
|||
|
||||
t.Run("UploadFileSuccess", func(t *testing.T) {
|
||||
// Create multipart request
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
req, err := createMultipartRequest(requestURL, "file", testFileName, []byte(testFileContent), map[string]string{
|
||||
"original_filename": testFileName,
|
||||
"path": "documents/reports/quarterly-report.txt",
|
||||
|
|
@ -177,7 +177,7 @@ func TestFileUpload(t *testing.T) {
|
|||
|
||||
t.Run("UploadFileWithCompression", func(t *testing.T) {
|
||||
// Test with gzip compression
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
req, err := createMultipartRequest(requestURL, "file", testFileName, []byte(testFileContent), map[string]string{
|
||||
"original_filename": testFileName,
|
||||
"gzip": "true",
|
||||
|
|
@ -205,7 +205,7 @@ func TestFileUpload(t *testing.T) {
|
|||
|
||||
t.Run("UploadFileInvalidUploader", func(t *testing.T) {
|
||||
// Test with invalid uploader ID
|
||||
requestURL := serverURL + baseURL + "/files/" + invalidUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + invalidUploaderID
|
||||
req, err := createMultipartRequest(requestURL, "file", testFileName, []byte(testFileContent), nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -228,7 +228,7 @@ func TestFileUpload(t *testing.T) {
|
|||
|
||||
t.Run("UploadFileNoFile", func(t *testing.T) {
|
||||
// Test with no file in request
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
req, err := http.NewRequest("POST", requestURL, strings.NewReader("no file data"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -252,7 +252,7 @@ func TestFileUpload(t *testing.T) {
|
|||
|
||||
t.Run("UploadFileMissingUploaderID", func(t *testing.T) {
|
||||
// Test with missing uploader ID in path
|
||||
requestURL := serverURL + baseURL + "/files/"
|
||||
requestURL := serverURL + baseURL + "/file/"
|
||||
req, err := createMultipartRequest(requestURL, "file", testFileName, []byte(testFileContent), nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -287,7 +287,7 @@ func TestFileChunkedUpload(t *testing.T) {
|
|||
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
||||
|
||||
t.Run("ChunkedUploadSuccess", func(t *testing.T) {
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
uid := fmt.Sprintf("chunked-test-%d", time.Now().UnixNano())
|
||||
|
||||
// Split content into chunks
|
||||
|
|
@ -353,7 +353,7 @@ func TestFileList(t *testing.T) {
|
|||
fileName := fmt.Sprintf("test-file-%d.txt", i)
|
||||
content := fmt.Sprintf("Test content for file %d", i)
|
||||
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
req, err := createMultipartRequest(requestURL, "file", fileName, []byte(content), map[string]string{
|
||||
"original_filename": fileName,
|
||||
})
|
||||
|
|
@ -378,7 +378,7 @@ func TestFileList(t *testing.T) {
|
|||
|
||||
t.Run("ListFilesSuccess", func(t *testing.T) {
|
||||
// Test basic file listing
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID, nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -405,7 +405,7 @@ func TestFileList(t *testing.T) {
|
|||
|
||||
t.Run("ListFilesWithPagination", func(t *testing.T) {
|
||||
// Test with pagination parameters
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"?page=1&page_size=2", nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"?page=1&page_size=2", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -427,7 +427,7 @@ func TestFileList(t *testing.T) {
|
|||
|
||||
t.Run("ListFilesWithFilters", func(t *testing.T) {
|
||||
// Test with filter parameters
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"?status=uploaded&content_type=text/plain", nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"?status=uploaded&content_type=text/plain", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -442,7 +442,7 @@ func TestFileList(t *testing.T) {
|
|||
|
||||
t.Run("ListFilesInvalidUploader", func(t *testing.T) {
|
||||
// Test with invalid uploader ID
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+invalidUploaderID, nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+invalidUploaderID, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -476,7 +476,7 @@ func TestFileRetrieve(t *testing.T) {
|
|||
|
||||
t.Run("SetupUploadFile", func(t *testing.T) {
|
||||
// Upload a file first
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
req, err := createMultipartRequest(requestURL, "file", testFileName, []byte(testFileContent), map[string]string{
|
||||
"original_filename": testFileName,
|
||||
})
|
||||
|
|
@ -499,7 +499,7 @@ func TestFileRetrieve(t *testing.T) {
|
|||
t.Run("RetrieveFileSuccess", func(t *testing.T) {
|
||||
// Retrieve file metadata
|
||||
encodedFileID := url.QueryEscape(testFileID)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -528,7 +528,7 @@ func TestFileRetrieve(t *testing.T) {
|
|||
// Test with non-existent file ID
|
||||
nonExistentID := "non-existent-file-id"
|
||||
encodedFileID := url.QueryEscape(nonExistentID)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -550,7 +550,7 @@ func TestFileRetrieve(t *testing.T) {
|
|||
t.Run("RetrieveFileMissingIDs", func(t *testing.T) {
|
||||
// Test with missing file ID - this URL actually matches the list endpoint
|
||||
// which is correct RESTful behavior, so we expect 200 OK
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/", nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -594,7 +594,7 @@ func TestFileContent(t *testing.T) {
|
|||
|
||||
t.Run("SetupUploadFile", func(t *testing.T) {
|
||||
// Upload a file first
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
req, err := createMultipartRequest(requestURL, "file", testFileName, []byte(testFileContent), map[string]string{
|
||||
"original_filename": testFileName,
|
||||
})
|
||||
|
|
@ -617,7 +617,7 @@ func TestFileContent(t *testing.T) {
|
|||
t.Run("GetFileContentSuccess", func(t *testing.T) {
|
||||
// Get file content
|
||||
encodedFileID := url.QueryEscape(testFileID)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID+"/content", nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID+"/content", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -641,7 +641,7 @@ func TestFileContent(t *testing.T) {
|
|||
// Test with non-existent file ID
|
||||
nonExistentID := "non-existent-file-id"
|
||||
encodedFileID := url.QueryEscape(nonExistentID)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID+"/content", nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID+"/content", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -676,7 +676,7 @@ func TestFileExists(t *testing.T) {
|
|||
|
||||
t.Run("SetupUploadFile", func(t *testing.T) {
|
||||
// Upload a file first
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
req, err := createMultipartRequest(requestURL, "file", testFileName, []byte(testFileContent), map[string]string{
|
||||
"original_filename": testFileName,
|
||||
})
|
||||
|
|
@ -699,7 +699,7 @@ func TestFileExists(t *testing.T) {
|
|||
t.Run("FileExistsTrue", func(t *testing.T) {
|
||||
// Check if uploaded file exists
|
||||
encodedFileID := url.QueryEscape(testFileID)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID+"/exists", nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID+"/exists", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -726,7 +726,7 @@ func TestFileExists(t *testing.T) {
|
|||
// Check if non-existent file exists
|
||||
nonExistentID := "non-existent-file-id"
|
||||
encodedFileID := url.QueryEscape(nonExistentID)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID+"/exists", nil)
|
||||
req, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID+"/exists", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -768,7 +768,7 @@ func TestFileDelete(t *testing.T) {
|
|||
|
||||
t.Run("DeleteFileSuccess", func(t *testing.T) {
|
||||
// Upload a file first
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
req, err := createMultipartRequest(requestURL, "file", testFileName, []byte(testFileContent), map[string]string{
|
||||
"original_filename": testFileName,
|
||||
})
|
||||
|
|
@ -788,7 +788,7 @@ func TestFileDelete(t *testing.T) {
|
|||
|
||||
// Now delete the file
|
||||
encodedFileID := url.QueryEscape(testFileID)
|
||||
deleteReq, err := http.NewRequest("DELETE", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
deleteReq, err := http.NewRequest("DELETE", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
assert.NoError(t, err)
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -814,7 +814,7 @@ func TestFileDelete(t *testing.T) {
|
|||
// Test deleting non-existent file
|
||||
nonExistentID := "non-existent-file-id"
|
||||
encodedFileID := url.QueryEscape(nonExistentID)
|
||||
req, err := http.NewRequest("DELETE", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
req, err := http.NewRequest("DELETE", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -850,12 +850,12 @@ func TestFileEndpointsUnauthorized(t *testing.T) {
|
|||
method string
|
||||
path string
|
||||
}{
|
||||
{"POST", "/files/" + testUploaderID},
|
||||
{"GET", "/files/" + testUploaderID},
|
||||
{"GET", "/files/" + testUploaderID + "/test-file-id"},
|
||||
{"DELETE", "/files/" + testUploaderID + "/test-file-id"},
|
||||
{"GET", "/files/" + testUploaderID + "/test-file-id/content"},
|
||||
{"GET", "/files/" + testUploaderID + "/test-file-id/exists"},
|
||||
{"POST", "/file/" + testUploaderID},
|
||||
{"GET", "/file/" + testUploaderID},
|
||||
{"GET", "/file/" + testUploaderID + "/test-file-id"},
|
||||
{"DELETE", "/file/" + testUploaderID + "/test-file-id"},
|
||||
{"GET", "/file/" + testUploaderID + "/test-file-id/content"},
|
||||
{"GET", "/file/" + testUploaderID + "/test-file-id/exists"},
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
|
|
@ -908,7 +908,7 @@ func TestFileIntegration(t *testing.T) {
|
|||
|
||||
t.Run("FullFileLifecycle", func(t *testing.T) {
|
||||
// Step 1: Upload a file
|
||||
requestURL := serverURL + baseURL + "/files/" + testUploaderID
|
||||
requestURL := serverURL + baseURL + "/file/" + testUploaderID
|
||||
uploadReq, err := createMultipartRequest(requestURL, "file", testFileName, []byte(testFileContent), map[string]string{
|
||||
"original_filename": testFileName,
|
||||
"path": "integration/test/file.txt",
|
||||
|
|
@ -931,7 +931,7 @@ func TestFileIntegration(t *testing.T) {
|
|||
|
||||
// Step 2: Verify file exists
|
||||
encodedFileID := url.QueryEscape(testFileID)
|
||||
existsReq, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID+"/exists", nil)
|
||||
existsReq, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID+"/exists", nil)
|
||||
assert.NoError(t, err)
|
||||
existsReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -942,7 +942,7 @@ func TestFileIntegration(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, existsResp.StatusCode)
|
||||
|
||||
// Step 3: Retrieve file metadata
|
||||
retrieveReq, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
retrieveReq, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
assert.NoError(t, err)
|
||||
retrieveReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -953,7 +953,7 @@ func TestFileIntegration(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, retrieveResp.StatusCode)
|
||||
|
||||
// Step 4: Download file content
|
||||
contentReq, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID+"/content", nil)
|
||||
contentReq, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID+"/content", nil)
|
||||
assert.NoError(t, err)
|
||||
contentReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -968,7 +968,7 @@ func TestFileIntegration(t *testing.T) {
|
|||
assert.Equal(t, testFileContent, string(content))
|
||||
|
||||
// Step 5: List files and verify our file is included
|
||||
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"?name="+testFileName, nil)
|
||||
listReq, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"?name="+testFileName, nil)
|
||||
assert.NoError(t, err)
|
||||
listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -979,7 +979,7 @@ func TestFileIntegration(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, listResp.StatusCode)
|
||||
|
||||
// Step 6: Delete the file
|
||||
deleteReq, err := http.NewRequest("DELETE", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
deleteReq, err := http.NewRequest("DELETE", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID, nil)
|
||||
assert.NoError(t, err)
|
||||
deleteReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
@ -990,7 +990,7 @@ func TestFileIntegration(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, deleteResp.StatusCode)
|
||||
|
||||
// Step 7: Verify file no longer exists
|
||||
finalExistsReq, err := http.NewRequest("GET", serverURL+baseURL+"/files/"+testUploaderID+"/"+encodedFileID+"/exists", nil)
|
||||
finalExistsReq, err := http.NewRequest("GET", serverURL+baseURL+"/file/"+testUploaderID+"/"+encodedFileID+"/exists", nil)
|
||||
assert.NoError(t, err)
|
||||
finalExistsReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
||||
|
||||
|
|
|
|||
201
openapi/tests/signin_test.go
Normal file
201
openapi/tests/signin_test.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
package openapi_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/openapi"
|
||||
"github.com/yaoapp/yao/openapi/signin"
|
||||
"github.com/yaoapp/yao/openapi/tests/testutils"
|
||||
)
|
||||
|
||||
func TestSigninLoad(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
_ = serverURL // Server URL not needed for this test
|
||||
|
||||
// Test loading signin configurations
|
||||
err := signin.Load(config.Conf)
|
||||
assert.NoError(t, err, "signin.Load should succeed")
|
||||
|
||||
// Test that we can get available languages
|
||||
languages := signin.GetAvailableLanguages()
|
||||
assert.IsType(t, []string{}, languages, "Should return string slice")
|
||||
t.Logf("Available languages: %v", languages)
|
||||
|
||||
// Test default language
|
||||
defaultLang := signin.GetDefaultLanguage()
|
||||
assert.IsType(t, "", defaultLang, "Should return string")
|
||||
t.Logf("Default language: %s", defaultLang)
|
||||
}
|
||||
|
||||
func TestSigninGetConfigs(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
_ = serverURL // Server URL not needed for this test
|
||||
|
||||
// Load signin configurations
|
||||
err := signin.Load(config.Conf)
|
||||
assert.NoError(t, err, "signin.Load should succeed")
|
||||
|
||||
// Test getting configs for different languages
|
||||
testCases := []string{"", "en", "zh-cn", "fr"}
|
||||
|
||||
for _, lang := range testCases {
|
||||
t.Run("lang_"+lang, func(t *testing.T) {
|
||||
fullConfig := signin.GetFullConfig(lang)
|
||||
publicConfig := signin.GetPublicConfig(lang)
|
||||
|
||||
if fullConfig != nil {
|
||||
t.Logf("Full config for '%s': %+v", lang, fullConfig.Title)
|
||||
assert.NotNil(t, publicConfig, "Public config should exist if full config exists")
|
||||
|
||||
// Test that public config removes sensitive data
|
||||
if fullConfig.ThirdParty != nil && fullConfig.ThirdParty.Providers != nil {
|
||||
for i := range fullConfig.ThirdParty.Providers {
|
||||
if publicConfig.ThirdParty != nil && i < len(publicConfig.ThirdParty.Providers) {
|
||||
publicProvider := publicConfig.ThirdParty.Providers[i]
|
||||
assert.Empty(t, publicProvider.ClientSecret, "Client secret should be empty in public config")
|
||||
assert.Nil(t, publicProvider.ClientSecretGenerator, "Client secret generator should be nil in public config")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Logf("No config found for language: %s", lang)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigninLanguageNormalization(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
_ = serverURL // Server URL not needed for this test
|
||||
|
||||
// Load signin configurations
|
||||
err := signin.Load(config.Conf)
|
||||
assert.NoError(t, err, "signin.Load should succeed")
|
||||
|
||||
// Test that language codes are normalized to lowercase
|
||||
config1 := signin.GetFullConfig("EN")
|
||||
config2 := signin.GetFullConfig("en")
|
||||
assert.Equal(t, config1, config2, "Language codes should be normalized to lowercase")
|
||||
|
||||
config3 := signin.GetPublicConfig("ZH-CN")
|
||||
config4 := signin.GetPublicConfig("zh-cn")
|
||||
assert.Equal(t, config3, config4, "Language codes should be normalized to lowercase")
|
||||
}
|
||||
|
||||
func TestSigninConfigStructure(t *testing.T) {
|
||||
// Initialize test environment
|
||||
serverURL := testutils.Prepare(t)
|
||||
defer testutils.Clean()
|
||||
|
||||
_ = serverURL // Server URL not needed for this test
|
||||
|
||||
// Load signin configurations
|
||||
err := signin.Load(config.Conf)
|
||||
assert.NoError(t, err, "signin.Load should succeed")
|
||||
|
||||
// Get a config to test structure
|
||||
config := signin.GetFullConfig("")
|
||||
if config != nil {
|
||||
t.Logf("Config loaded successfully with title: %s", config.Title)
|
||||
|
||||
// Verify config structure is valid
|
||||
assert.IsType(t, &signin.Config{}, config, "Should return correct config type")
|
||||
|
||||
// Test form configuration
|
||||
if config.Form != nil {
|
||||
t.Logf("Form configuration found")
|
||||
if config.Form.Username != nil {
|
||||
assert.IsType(t, []string{}, config.Form.Username.Fields, "Username fields should be string slice")
|
||||
}
|
||||
if config.Form.Captcha != nil {
|
||||
assert.IsType(t, map[string]interface{}{}, config.Form.Captcha.Options, "Captcha options should be map")
|
||||
}
|
||||
}
|
||||
|
||||
// Test third party configuration
|
||||
if config.ThirdParty != nil {
|
||||
t.Logf("Third party configuration found with %d providers", len(config.ThirdParty.Providers))
|
||||
if config.ThirdParty.Providers != nil {
|
||||
assert.IsType(t, []*signin.Provider{}, config.ThirdParty.Providers, "Providers should be slice of Provider pointers")
|
||||
for i, provider := range config.ThirdParty.Providers {
|
||||
t.Logf("Provider %d: %s", i, provider.ID)
|
||||
assert.IsType(t, []string{}, provider.Scopes, "Provider scopes should be string slice")
|
||||
assert.IsType(t, map[string]string{}, provider.Mapping, "Provider mapping should be string map")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Log("No signin configuration found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigninAPI(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
|
||||
}
|
||||
|
||||
// Test API endpoints
|
||||
testCases := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
expectCode int
|
||||
}{
|
||||
{"get config without locale", "/signin", 200},
|
||||
{"get config with en locale", "/signin?locale=en", 200},
|
||||
{"get config with zh-cn locale", "/signin?locale=zh-cn", 200},
|
||||
{"get config with invalid locale", "/signin?locale=invalid", 200}, // should fallback to default
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
url := serverURL + baseURL + tc.endpoint
|
||||
resp, err := http.Get(url)
|
||||
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", tc.expectCode)
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
// Parse response body
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err, "Should read response body")
|
||||
|
||||
var config signin.Config
|
||||
err = json.Unmarshal(body, &config)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
|
||||
t.Logf("API response for %s: %s", tc.endpoint, config.Title)
|
||||
|
||||
// Verify it's public config (no sensitive data)
|
||||
if config.ThirdParty != nil && config.ThirdParty.Providers != nil {
|
||||
for _, provider := range config.ThirdParty.Providers {
|
||||
assert.Empty(t, provider.ClientSecret, "Client secret should be empty in API response")
|
||||
assert.Nil(t, provider.ClientSecretGenerator, "Client secret generator should be nil in API response")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue