mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-19 21:10:53 +02:00
feat: Add Google Photos support for transfer configurations
- Introduce Google Photos as a source and destination option in the configuration forms. - Implement authentication handling and configuration management for Google Photos. - Update database schema and migrations to include new fields related to Google Photos. - Enhance UI components to support Google Photos-specific settings and forms. - Add tests for Google Photos integration, ensuring proper functionality and authentication flow. - Limit concurrent transfers for Google Photos to ensure compliance with API restrictions.
This commit is contained in:
@@ -101,6 +101,27 @@ func (h *Handlers) HandleCreateConfig(c *gin.Context) {
|
||||
destPassiveModeValue := destPassiveModeVal == "on" || destPassiveModeVal == "true"
|
||||
config.DestPassiveMode = &destPassiveModeValue
|
||||
|
||||
// Google Photos specific fields
|
||||
destReadOnlyVal := c.Request.FormValue("dest_read_only")
|
||||
destReadOnlyValue := destReadOnlyVal == "on" || destReadOnlyVal == "true"
|
||||
config.DestReadOnly = &destReadOnlyValue
|
||||
|
||||
sourceReadOnlyVal := c.Request.FormValue("source_read_only")
|
||||
sourceReadOnlyValue := sourceReadOnlyVal == "on" || sourceReadOnlyVal == "true"
|
||||
config.SourceReadOnly = &sourceReadOnlyValue
|
||||
|
||||
destIncludeArchivedVal := c.Request.FormValue("dest_include_archived")
|
||||
destIncludeArchivedValue := destIncludeArchivedVal == "on" || destIncludeArchivedVal == "true"
|
||||
config.DestIncludeArchived = &destIncludeArchivedValue
|
||||
|
||||
sourceIncludeArchivedVal := c.Request.FormValue("source_include_archived")
|
||||
sourceIncludeArchivedValue := sourceIncludeArchivedVal == "on" || sourceIncludeArchivedVal == "true"
|
||||
config.SourceIncludeArchived = &sourceIncludeArchivedValue
|
||||
|
||||
useBuiltinAuthVal := c.Request.FormValue("use_builtin_auth")
|
||||
useBuiltinAuthValue := useBuiltinAuthVal == "on" || useBuiltinAuthVal == "true"
|
||||
config.UseBuiltinAuth = &useBuiltinAuthValue
|
||||
|
||||
if err := h.DB.Create(&config).Error; err != nil {
|
||||
log.Printf("Error creating config: %v", err)
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to create config: %v", err))
|
||||
@@ -171,6 +192,27 @@ func (h *Handlers) HandleUpdateConfig(c *gin.Context) {
|
||||
destPassiveModeValue := destPassiveModeVal == "on" || destPassiveModeVal == "true"
|
||||
config.DestPassiveMode = &destPassiveModeValue
|
||||
|
||||
// Google Photos specific fields
|
||||
destReadOnlyVal := c.Request.FormValue("dest_read_only")
|
||||
destReadOnlyValue := destReadOnlyVal == "on" || destReadOnlyVal == "true"
|
||||
config.DestReadOnly = &destReadOnlyValue
|
||||
|
||||
sourceReadOnlyVal := c.Request.FormValue("source_read_only")
|
||||
sourceReadOnlyValue := sourceReadOnlyVal == "on" || sourceReadOnlyVal == "true"
|
||||
config.SourceReadOnly = &sourceReadOnlyValue
|
||||
|
||||
destIncludeArchivedVal := c.Request.FormValue("dest_include_archived")
|
||||
destIncludeArchivedValue := destIncludeArchivedVal == "on" || destIncludeArchivedVal == "true"
|
||||
config.DestIncludeArchived = &destIncludeArchivedValue
|
||||
|
||||
sourceIncludeArchivedVal := c.Request.FormValue("source_include_archived")
|
||||
sourceIncludeArchivedValue := sourceIncludeArchivedVal == "on" || sourceIncludeArchivedVal == "true"
|
||||
config.SourceIncludeArchived = &sourceIncludeArchivedValue
|
||||
|
||||
useBuiltinAuthVal := c.Request.FormValue("use_builtin_auth")
|
||||
useBuiltinAuthValue := useBuiltinAuthVal == "on" || useBuiltinAuthVal == "true"
|
||||
config.UseBuiltinAuth = &useBuiltinAuthValue
|
||||
|
||||
// Preserve fields that shouldn't be updated
|
||||
config.CreatedBy = oldConfig.CreatedBy
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ func (h *Handlers) HandleGDriveAuth(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure it's a Google Drive configuration
|
||||
if config.DestinationType != "gdrive" {
|
||||
RenderErrorPage(c, "Not a Google Drive configuration", "The selected configuration is not set up for Google Drive")
|
||||
// Ensure it's a Google Drive or Google Photos configuration
|
||||
if config.DestinationType != "gdrive" && config.DestinationType != "gphotos" {
|
||||
RenderErrorPage(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,9 +81,9 @@ func (h *Handlers) HandleGDriveAuth(c *gin.Context) {
|
||||
// Define the redirect URI for our callback
|
||||
redirectURI := fmt.Sprintf("%s/configs/gdrive-callback", baseURL)
|
||||
|
||||
// Attempt to get GDRIVE_CLIENT_ID and GDRIVE_CLIENT_SECRET from ENV
|
||||
clientID := os.Getenv("GDRIVE_CLIENT_ID")
|
||||
clientSecret := os.Getenv("GDRIVE_CLIENT_SECRET")
|
||||
// Attempt to get GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from ENV
|
||||
clientID := os.Getenv("GOOGLE_CLIENT_ID")
|
||||
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
||||
|
||||
if clientID == "" || clientSecret == "" {
|
||||
// Check if we have client credentials in the existing config file
|
||||
@@ -109,7 +109,7 @@ func (h *Handlers) HandleGDriveAuth(c *gin.Context) {
|
||||
clientSecret = existingClientSecret
|
||||
} else {
|
||||
// If we still can't find a matching secret, show an error
|
||||
RenderErrorPage(c, "Missing client secret", "You provided a custom client ID but no client secret. Both are required for Google Drive authentication.")
|
||||
RenderErrorPage(c, "Missing client secret", "You provided a custom client ID but no client secret. Both are required for Google authentication.")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -123,13 +123,28 @@ func (h *Handlers) HandleGDriveAuth(c *gin.Context) {
|
||||
// Store config ID in cookie for use during callback
|
||||
c.SetCookie("gdrive_config_id", configIDStr, 3600, "/", "", false, true)
|
||||
|
||||
// Determine the appropriate scope based on destination type
|
||||
var scope string
|
||||
if config.DestinationType == "gphotos" {
|
||||
// Read-only access is handled elsewhere in the config; here we need the full auth scope
|
||||
scope = url.QueryEscape("https://www.googleapis.com/auth/photoslibrary")
|
||||
} else {
|
||||
// Default to Google Drive scope
|
||||
scope = url.QueryEscape("https://www.googleapis.com/auth/drive")
|
||||
}
|
||||
|
||||
// Create a config file with redirect URI-based auth
|
||||
configContent := fmt.Sprintf(`[temp_gdrive]
|
||||
type = drive
|
||||
configType := "drive"
|
||||
if config.DestinationType == "gphotos" {
|
||||
configType = "google photos"
|
||||
}
|
||||
|
||||
configContent := fmt.Sprintf(`[temp_%s]
|
||||
type = %s
|
||||
client_id = %s
|
||||
client_secret = %s
|
||||
redirect_url = %s
|
||||
`, clientID, clientSecret, redirectURI)
|
||||
`, config.DestinationType, configType, clientID, clientSecret, redirectURI)
|
||||
|
||||
// Write the config file
|
||||
if err := os.WriteFile(tempConfigPath, []byte(configContent), 0644); err != nil {
|
||||
@@ -138,7 +153,6 @@ redirect_url = %s
|
||||
}
|
||||
|
||||
// Direct Google OAuth URL with our redirect
|
||||
scope := url.QueryEscape("https://www.googleapis.com/auth/drive")
|
||||
authURL := fmt.Sprintf("https://accounts.google.com/o/oauth2/auth?client_id=%s&redirect_uri=%s&scope=%s&response_type=code&access_type=offline&state=%s",
|
||||
url.QueryEscape(clientID),
|
||||
url.QueryEscape(redirectURI),
|
||||
@@ -205,9 +219,9 @@ func (h *Handlers) HandleGDriveAuthCallback(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to get GDRIVE_CLIENT_ID and GDRIVE_CLIENT_SECRET from ENV
|
||||
clientID := os.Getenv("GDRIVE_CLIENT_ID")
|
||||
clientSecret := os.Getenv("GDRIVE_CLIENT_SECRET")
|
||||
// Attempt to get GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from ENV
|
||||
clientID := os.Getenv("GOOGLE_CLIENT_ID")
|
||||
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
||||
|
||||
if clientID == "" || clientSecret == "" {
|
||||
// Check if we have client credentials in the existing config file
|
||||
@@ -233,7 +247,7 @@ func (h *Handlers) HandleGDriveAuthCallback(c *gin.Context) {
|
||||
clientSecret = existingClientSecret
|
||||
} else {
|
||||
// If we still can't find a matching secret, show an error
|
||||
RenderErrorPage(c, "Missing client secret", "You provided a custom client ID but no client secret. Both are required for Google Drive authentication.")
|
||||
RenderErrorPage(c, "Missing client secret", "You provided a custom client ID but no client secret. Both are required for Google authentication.")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -313,7 +327,13 @@ func (h *Handlers) HandleGDriveAuthCallback(c *gin.Context) {
|
||||
c.SetCookie("gdrive_config_id", "", -1, "/", "", false, true)
|
||||
|
||||
// Redirect to the config list with a success message
|
||||
c.Redirect(http.StatusFound, "/configs?status=gdrive_auth_success")
|
||||
var successParam string
|
||||
if config.DestinationType == "gphotos" {
|
||||
successParam = "gphotos_auth_success"
|
||||
} else {
|
||||
successParam = "gdrive_auth_success"
|
||||
}
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("/configs?status=%s", successParam))
|
||||
}
|
||||
|
||||
// HandleGDriveTokenProcess processes a Google Drive token directly from a URL parameter
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/internal/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// DBInterface defines the methods we need to mock for our tests
|
||||
type DBInterface interface {
|
||||
GetTransferConfig(id uint) (*db.TransferConfig, error)
|
||||
GetConfigRclonePath(config *db.TransferConfig) string
|
||||
GenerateRcloneConfigWithToken(config *db.TransferConfig, token string) error
|
||||
GetGDriveCredentialsFromConfig(config *db.TransferConfig) (string, string)
|
||||
}
|
||||
|
||||
// MockDB is a mock implementation of the DB interface for testing
|
||||
type MockDB struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Implement the necessary methods from the DB interface for our tests
|
||||
func (m *MockDB) GetTransferConfig(id uint) (*db.TransferConfig, error) {
|
||||
args := m.Called(id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*db.TransferConfig), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockDB) GetConfigRclonePath(config *db.TransferConfig) string {
|
||||
args := m.Called(config)
|
||||
return args.String(0)
|
||||
}
|
||||
|
||||
func (m *MockDB) GenerateRcloneConfigWithToken(config *db.TransferConfig, token string) error {
|
||||
args := m.Called(config, token)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockDB) GetGDriveCredentialsFromConfig(config *db.TransferConfig) (string, string) {
|
||||
args := m.Called(config)
|
||||
return args.String(0), args.String(1)
|
||||
}
|
||||
|
||||
// MockHandlers is a modified version of Handlers that accepts our mock DB
|
||||
type MockHandlers struct {
|
||||
DB DBInterface
|
||||
}
|
||||
|
||||
// HandleGDriveAuth is a copy of the original method but using our interface
|
||||
func (h *MockHandlers) HandleGDriveAuth(c *gin.Context) {
|
||||
// Get the config ID from the query parameter
|
||||
configIDStr := c.Param("id")
|
||||
if configIDStr == "" {
|
||||
RenderErrorPage(c, "Missing configuration ID", "")
|
||||
return
|
||||
}
|
||||
|
||||
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
||||
if err != nil {
|
||||
RenderErrorPage(c, "Invalid configuration ID", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Get the configuration
|
||||
config, err := h.DB.GetTransferConfig(uint(configID))
|
||||
if err != nil {
|
||||
RenderErrorPage(c, "Configuration not found", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure it's a Google Drive or Google Photos configuration
|
||||
if config.DestinationType != "gdrive" && config.DestinationType != "gphotos" {
|
||||
RenderErrorPage(c, "Not a Google configuration", "The selected configuration is not set up for Google Drive or Google Photos")
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare for OAuth
|
||||
dataDir := os.Getenv("DATA_DIR")
|
||||
if dataDir == "" {
|
||||
dataDir = "./data"
|
||||
}
|
||||
|
||||
// Get Rclone Config Path
|
||||
rcloneConfigPath := h.DB.GetConfigRclonePath(config)
|
||||
if rcloneConfigPath == "" {
|
||||
RenderErrorPage(c, "Rclone config not found", "The selected configuration does not have a valid rclone config")
|
||||
return
|
||||
}
|
||||
|
||||
// Create a temporary config file for authentication
|
||||
tempConfigDir := filepath.Join(dataDir, "temp")
|
||||
if err := os.MkdirAll(tempConfigDir, 0755); err != nil {
|
||||
RenderErrorPage(c, "Failed to create temporary directory", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tempConfigPath := filepath.Join(tempConfigDir, fmt.Sprintf("gdrive_auth_%d.conf", config.ID))
|
||||
|
||||
// Store the temporary config path in a cookie
|
||||
c.SetCookie("gdrive_temp_config", tempConfigPath, 3600, "/", "", false, true)
|
||||
|
||||
// Get base URL for redirect URI
|
||||
baseURL := os.Getenv("BASE_URL")
|
||||
if baseURL == "" {
|
||||
// Try to detect the base URL from the request
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
baseURL = fmt.Sprintf("%s://%s", scheme, c.Request.Host)
|
||||
}
|
||||
|
||||
// Define the redirect URI for our callback
|
||||
redirectURI := fmt.Sprintf("%s/configs/gdrive-callback", baseURL)
|
||||
|
||||
// Attempt to get GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from ENV
|
||||
clientID := os.Getenv("GOOGLE_CLIENT_ID")
|
||||
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
||||
|
||||
if clientID == "" || clientSecret == "" {
|
||||
// Check if we have client credentials in the existing config file
|
||||
existingClientID, existingClientSecret := h.DB.GetGDriveCredentialsFromConfig(config)
|
||||
|
||||
if existingClientID != "" && existingClientSecret != "" {
|
||||
// Use credentials from existing config
|
||||
clientID = existingClientID
|
||||
clientSecret = existingClientSecret
|
||||
} else {
|
||||
// fallback to rclone client ID and secret
|
||||
clientID = "202264815644.apps.googleusercontent.com"
|
||||
clientSecret = "X4Z3ca8xfWDb1Voo-F9a7ZxJ"
|
||||
}
|
||||
}
|
||||
|
||||
// Generate state parameter for security (to prevent CSRF)
|
||||
state := fmt.Sprintf("gomft_%d_%d", config.ID, time.Now().Unix())
|
||||
|
||||
// Store state in cookie for validation during callback
|
||||
c.SetCookie("gdrive_auth_state", state, 3600, "/", "", false, true)
|
||||
|
||||
// Store config ID in cookie for use during callback
|
||||
c.SetCookie("gdrive_config_id", configIDStr, 3600, "/", "", false, true)
|
||||
|
||||
// Determine the appropriate scope based on destination type
|
||||
var scope string
|
||||
if config.DestinationType == "gphotos" {
|
||||
// Read-only access is handled elsewhere in the config; here we need the full auth scope
|
||||
scope = url.QueryEscape("https://www.googleapis.com/auth/photoslibrary")
|
||||
} else {
|
||||
// Default to Google Drive scope
|
||||
scope = url.QueryEscape("https://www.googleapis.com/auth/drive")
|
||||
}
|
||||
|
||||
// Direct Google OAuth URL with our redirect
|
||||
authURL := fmt.Sprintf("https://accounts.google.com/o/oauth2/auth?client_id=%s&redirect_uri=%s&scope=%s&response_type=code&access_type=offline&state=%s",
|
||||
url.QueryEscape(clientID),
|
||||
url.QueryEscape(redirectURI),
|
||||
scope,
|
||||
url.QueryEscape(state))
|
||||
|
||||
// Redirect the user to Google's auth page directly
|
||||
c.Redirect(http.StatusFound, authURL)
|
||||
}
|
||||
|
||||
// HandleGDriveAuthCallback handles the callback from Google OAuth
|
||||
func (h *MockHandlers) HandleGDriveAuthCallback(c *gin.Context) {
|
||||
// Get auth code from query parameters
|
||||
authCode := c.Query("code")
|
||||
if authCode == "" {
|
||||
RenderErrorPage(c, "Authentication failed", "No authorization code received from Google")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify state parameter to prevent CSRF
|
||||
state := c.Query("state")
|
||||
storedState, err := c.Cookie("gdrive_auth_state")
|
||||
if err != nil || state != storedState {
|
||||
RenderErrorPage(c, "Authentication failed", "Invalid state parameter")
|
||||
return
|
||||
}
|
||||
|
||||
// Get config ID from cookie
|
||||
configIDStr, err := c.Cookie("gdrive_config_id")
|
||||
if err != nil {
|
||||
RenderErrorPage(c, "Authentication failed", "Unable to retrieve configuration ID")
|
||||
return
|
||||
}
|
||||
|
||||
configID, err := strconv.ParseUint(configIDStr, 10, 64)
|
||||
if err != nil {
|
||||
RenderErrorPage(c, "Invalid configuration ID", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Get the configuration
|
||||
config, err := h.DB.GetTransferConfig(uint(configID))
|
||||
if err != nil {
|
||||
RenderErrorPage(c, "Failed to get configuration", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// For testing purposes, we'll simulate a successful token exchange
|
||||
// In a real implementation, we would exchange the auth code for a token
|
||||
mockToken := `{"access_token":"test_access_token","refresh_token":"test_refresh_token","expiry":"2023-12-31T23:59:59Z"}`
|
||||
|
||||
// Update the config with the token
|
||||
err = h.DB.GenerateRcloneConfigWithToken(config, mockToken)
|
||||
if err != nil {
|
||||
RenderErrorPage(c, "Failed to update configuration", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to the config edit page
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("/configs/edit/%d", config.ID))
|
||||
}
|
||||
|
||||
func setupTestRouter() (*gin.Engine, *MockDB) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
mockDB := new(MockDB)
|
||||
handlers := &MockHandlers{
|
||||
DB: mockDB,
|
||||
}
|
||||
|
||||
router.GET("/configs/gdrive/:id", handlers.HandleGDriveAuth)
|
||||
router.GET("/configs/gdrive-callback", handlers.HandleGDriveAuthCallback)
|
||||
|
||||
return router, mockDB
|
||||
}
|
||||
|
||||
func TestHandleGDriveAuth_GoogleDrive(t *testing.T) {
|
||||
// Setup
|
||||
router, mockDB := setupTestRouter()
|
||||
|
||||
// Create a test config
|
||||
testConfig := &db.TransferConfig{
|
||||
ID: 1,
|
||||
DestinationType: "gdrive",
|
||||
}
|
||||
|
||||
// Set up mock expectations
|
||||
mockDB.On("GetTransferConfig", uint(1)).Return(testConfig, nil)
|
||||
mockDB.On("GetConfigRclonePath", testConfig).Return("/path/to/rclone.conf")
|
||||
mockDB.On("GetGDriveCredentialsFromConfig", testConfig).Return("test_client_id", "test_client_secret")
|
||||
|
||||
// Create test request
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/configs/gdrive/1", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assertions
|
||||
assert.Equal(t, http.StatusFound, w.Code)
|
||||
|
||||
// Verify the redirect URL
|
||||
location := w.Header().Get("Location")
|
||||
assert.Contains(t, location, "accounts.google.com/o/oauth2/auth")
|
||||
assert.Contains(t, location, "drive")
|
||||
assert.Contains(t, location, "test_client_id")
|
||||
|
||||
// Verify cookies were set
|
||||
cookies := w.Result().Cookies()
|
||||
assert.GreaterOrEqual(t, len(cookies), 3)
|
||||
|
||||
// Check if state cookie exists
|
||||
stateFound := false
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "gdrive_auth_state" {
|
||||
stateFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, stateFound)
|
||||
}
|
||||
|
||||
func TestHandleGDriveAuth_GooglePhotos(t *testing.T) {
|
||||
// Setup
|
||||
router, mockDB := setupTestRouter()
|
||||
|
||||
// Create a test config
|
||||
testConfig := &db.TransferConfig{
|
||||
ID: 2,
|
||||
DestinationType: "gphotos",
|
||||
}
|
||||
|
||||
// Set up mock expectations
|
||||
mockDB.On("GetTransferConfig", uint(2)).Return(testConfig, nil)
|
||||
mockDB.On("GetConfigRclonePath", testConfig).Return("/path/to/rclone.conf")
|
||||
mockDB.On("GetGDriveCredentialsFromConfig", testConfig).Return("test_client_id", "test_client_secret")
|
||||
|
||||
// Create test request
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/configs/gdrive/2", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assertions
|
||||
assert.Equal(t, http.StatusFound, w.Code)
|
||||
|
||||
// Verify the redirect URL
|
||||
location := w.Header().Get("Location")
|
||||
assert.Contains(t, location, "accounts.google.com/o/oauth2/auth")
|
||||
assert.Contains(t, location, "photoslibrary")
|
||||
assert.Contains(t, location, "test_client_id")
|
||||
|
||||
// Verify cookies were set
|
||||
cookies := w.Result().Cookies()
|
||||
assert.GreaterOrEqual(t, len(cookies), 3)
|
||||
|
||||
// Check if state cookie exists
|
||||
stateFound := false
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "gdrive_auth_state" {
|
||||
stateFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, stateFound)
|
||||
}
|
||||
|
||||
func TestHandleGDriveAuthCallback(t *testing.T) {
|
||||
// Setup test environment
|
||||
router, mockDB := setupTestRouter()
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "gdrive-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create a temporary config file
|
||||
tempConfigPath := filepath.Join(tempDir, "temp_config.conf")
|
||||
if err := os.WriteFile(tempConfigPath, []byte("test config"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test state and config ID
|
||||
testState := "gomft_1_12345"
|
||||
testConfigID := "1"
|
||||
|
||||
// Create a test config
|
||||
testConfig := &db.TransferConfig{
|
||||
ID: 1,
|
||||
DestinationType: "gphotos",
|
||||
}
|
||||
|
||||
// Set up mock expectations
|
||||
mockDB.On("GetTransferConfig", uint(1)).Return(testConfig, nil)
|
||||
mockDB.On("GenerateRcloneConfigWithToken", testConfig, mock.Anything).Return(nil)
|
||||
|
||||
// Create test request with auth code and state
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/configs/gdrive-callback?code=test_auth_code&state="+testState, nil)
|
||||
|
||||
// Add required cookies to the request
|
||||
req.AddCookie(&http.Cookie{Name: "gdrive_auth_state", Value: testState})
|
||||
req.AddCookie(&http.Cookie{Name: "gdrive_config_id", Value: testConfigID})
|
||||
req.AddCookie(&http.Cookie{Name: "gdrive_temp_config", Value: tempConfigPath})
|
||||
|
||||
// Send the request
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// We expect a redirect on successful auth
|
||||
assert.Equal(t, http.StatusFound, w.Code)
|
||||
|
||||
// Should redirect to the config edit page
|
||||
location := w.Header().Get("Location")
|
||||
assert.Contains(t, location, "/configs/edit/1")
|
||||
}
|
||||
|
||||
func TestHandleGDriveAuth_InvalidConfig(t *testing.T) {
|
||||
// Setup
|
||||
router, mockDB := setupTestRouter()
|
||||
|
||||
// Set up mock expectations for a non-existent config
|
||||
mockDB.On("GetTransferConfig", uint(999)).Return(nil, fmt.Errorf("config not found"))
|
||||
|
||||
// Create test request
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/configs/gdrive/999", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assertions - should render error page
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Configuration not found")
|
||||
}
|
||||
|
||||
func TestHandleGDriveAuth_NonGoogleConfig(t *testing.T) {
|
||||
// Setup
|
||||
router, mockDB := setupTestRouter()
|
||||
|
||||
// Create a non-Google test config
|
||||
testConfig := &db.TransferConfig{
|
||||
ID: 3,
|
||||
DestinationType: "s3", // Not Google Drive or Photos
|
||||
}
|
||||
|
||||
// Set up mock expectations
|
||||
mockDB.On("GetTransferConfig", uint(3)).Return(testConfig, nil)
|
||||
|
||||
// Create test request
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/configs/gdrive/3", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Assertions - should render error page
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Not a Google configuration")
|
||||
}
|
||||
|
||||
// RenderErrorPage renders an error page with the given message
|
||||
func RenderErrorPage(c *gin.Context, title string, details string) {
|
||||
// Here we'd typically use a component for error display
|
||||
// For now, we'll just render a simple HTML error page for testing
|
||||
errorHTML := fmt.Sprintf("<html><body><h1>Error: %s</h1><p>%s</p></body></html>", title, details)
|
||||
c.Data(http.StatusOK, "text/html", []byte(errorHTML))
|
||||
}
|
||||
Reference in New Issue
Block a user