mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
287 lines
8.7 KiB
Go
287 lines
8.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/trackeep/backend/config"
|
|
"github.com/trackeep/backend/models"
|
|
"github.com/trackeep/backend/utils"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupGitHubAuthTestDB(t *testing.T, migrate ...interface{}) *gorm.DB {
|
|
t.Helper()
|
|
|
|
dsn := "file:" + url.PathEscape(t.Name()) + "?mode=memory&cache=shared"
|
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("failed to open sqlite database: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(migrate...); err != nil {
|
|
t.Fatalf("failed to migrate test database: %v", err)
|
|
}
|
|
|
|
previousDB := config.DB
|
|
config.DB = db
|
|
t.Cleanup(func() {
|
|
config.DB = previousDB
|
|
})
|
|
|
|
t.Setenv("VITE_DEMO_MODE", "false")
|
|
t.Setenv("JWT_SECRET", strings.Repeat("a", 64))
|
|
t.Setenv("ENCRYPTION_KEY", "test-encryption-key")
|
|
|
|
return db
|
|
}
|
|
|
|
func withControlServiceBaseURL(t *testing.T, value string) {
|
|
t.Helper()
|
|
|
|
previous := controlServiceBaseURL
|
|
controlServiceBaseURL = value
|
|
t.Cleanup(func() {
|
|
controlServiceBaseURL = previous
|
|
})
|
|
}
|
|
|
|
func TestGitHubLoginRedirectsToControlService(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
withControlServiceBaseURL(t, "https://control.example.com")
|
|
t.Setenv("PUBLIC_API_URL", "https://api.example.com")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/github?frontend_redirect="+url.QueryEscape("https://app.example.com/auth/callback"), nil)
|
|
rec := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(rec)
|
|
ctx.Request = req
|
|
|
|
GitHubLogin(ctx)
|
|
|
|
if rec.Code != http.StatusTemporaryRedirect {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
|
|
location := rec.Header().Get("Location")
|
|
parsed, err := url.Parse(location)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse redirect location: %v", err)
|
|
}
|
|
if parsed.Scheme != "https" || parsed.Host != "control.example.com" || parsed.Path != "/auth/github" {
|
|
t.Fatalf("unexpected redirect location: %s", location)
|
|
}
|
|
if got := parsed.Query().Get("redirect_uri"); got != "https://api.example.com/api/v1/auth/control/callback" {
|
|
t.Fatalf("unexpected redirect_uri: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestHandleOAuthCallbackStoresControllerSessionAndRedirects(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupGitHubAuthTestDB(t, &models.User{}, &models.ControlServiceSession{})
|
|
|
|
controller := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/auth/control/callback" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
_ = json.NewEncoder(w).Encode(controlServiceTokenValidationResponse{
|
|
Token: "controller-token-fresh",
|
|
User: centralizedOAuthUser{
|
|
ID: 77,
|
|
GitHubID: 99,
|
|
Username: "octocat",
|
|
Email: "octo@example.com",
|
|
Name: "The Octocat",
|
|
AvatarURL: "https://example.com/octocat.png",
|
|
},
|
|
})
|
|
}))
|
|
defer controller.Close()
|
|
withControlServiceBaseURL(t, controller.URL)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/control/callback?token=controller-token-old", nil)
|
|
req.AddCookie(&http.Cookie{Name: controlServiceFrontendRedirectCookieName, Value: "https://app.example.com/auth/callback"})
|
|
rec := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(rec)
|
|
ctx.Request = req
|
|
|
|
HandleOAuthCallback(ctx)
|
|
|
|
if rec.Code != http.StatusTemporaryRedirect {
|
|
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
location := rec.Header().Get("Location")
|
|
if !strings.HasPrefix(location, "https://app.example.com/auth/callback?token=") {
|
|
t.Fatalf("unexpected redirect location: %s", location)
|
|
}
|
|
|
|
var user models.User
|
|
if err := db.Where("github_id = ?", 99).First(&user).Error; err != nil {
|
|
t.Fatalf("failed to load local user: %v", err)
|
|
}
|
|
|
|
var session models.ControlServiceSession
|
|
if err := db.Where("user_id = ?", user.ID).First(&session).Error; err != nil {
|
|
t.Fatalf("failed to load controller session: %v", err)
|
|
}
|
|
decryptedToken, err := utils.Decrypt(session.Token)
|
|
if err != nil {
|
|
t.Fatalf("failed to decrypt controller token: %v", err)
|
|
}
|
|
if decryptedToken != "controller-token-fresh" {
|
|
t.Fatalf("unexpected stored controller token: %s", decryptedToken)
|
|
}
|
|
}
|
|
|
|
func TestGetGitHubReposUsesControlServiceAndPersistsRefreshedToken(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupGitHubAuthTestDB(t, &models.User{}, &models.ControlServiceSession{})
|
|
|
|
user := models.User{
|
|
Email: "octo@example.com",
|
|
Username: "octocat",
|
|
Password: "hashed-password",
|
|
FullName: "Octocat",
|
|
GitHubID: 99,
|
|
}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
encryptedToken, err := utils.Encrypt("controller-token-old")
|
|
if err != nil {
|
|
t.Fatalf("failed to encrypt controller token: %v", err)
|
|
}
|
|
if err := db.Create(&models.ControlServiceSession{
|
|
UserID: user.ID,
|
|
ControllerUserID: 77,
|
|
GitHubID: 99,
|
|
Username: "octocat",
|
|
Email: "octo@example.com",
|
|
Token: encryptedToken,
|
|
}).Error; err != nil {
|
|
t.Fatalf("failed to create controller session: %v", err)
|
|
}
|
|
|
|
controller := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/github/repos" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if got := r.Header.Get("Authorization"); got != "Bearer controller-token-old" {
|
|
t.Fatalf("unexpected authorization header: %s", got)
|
|
}
|
|
w.Header().Set(controlServiceSessionTokenHeader, "controller-token-new")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"repos": []GitHubRepo{{
|
|
ID: 1,
|
|
Name: "trackeep",
|
|
FullName: "octocat/trackeep",
|
|
}},
|
|
})
|
|
}))
|
|
defer controller.Close()
|
|
withControlServiceBaseURL(t, controller.URL)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/github/repos", nil)
|
|
rec := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(rec)
|
|
ctx.Request = req
|
|
ctx.Set("user_id", user.ID)
|
|
|
|
GetGitHubRepos(ctx)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "octocat/trackeep") {
|
|
t.Fatalf("unexpected response body: %s", rec.Body.String())
|
|
}
|
|
|
|
var updated models.ControlServiceSession
|
|
if err := db.Where("user_id = ?", user.ID).First(&updated).Error; err != nil {
|
|
t.Fatalf("failed to reload controller session: %v", err)
|
|
}
|
|
decryptedToken, err := utils.Decrypt(updated.Token)
|
|
if err != nil {
|
|
t.Fatalf("failed to decrypt refreshed controller token: %v", err)
|
|
}
|
|
if decryptedToken != "controller-token-new" {
|
|
t.Fatalf("unexpected refreshed controller token: %s", decryptedToken)
|
|
}
|
|
}
|
|
|
|
func TestGitHubAppInstallCallbackRejectsInaccessibleInstallationViaControlService(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupGitHubAuthTestDB(t, &models.User{}, &models.ControlServiceSession{}, &models.GitHubAppInstallState{})
|
|
t.Setenv("FRONTEND_URL", "https://app.example.com")
|
|
|
|
user := models.User{
|
|
Email: "octo@example.com",
|
|
Username: "octocat",
|
|
Password: "hashed-password",
|
|
FullName: "Octocat",
|
|
GitHubID: 99,
|
|
}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
encryptedToken, err := utils.Encrypt("controller-token-old")
|
|
if err != nil {
|
|
t.Fatalf("failed to encrypt controller token: %v", err)
|
|
}
|
|
if err := db.Create(&models.ControlServiceSession{
|
|
UserID: user.ID,
|
|
ControllerUserID: 77,
|
|
GitHubID: 99,
|
|
Username: "octocat",
|
|
Email: "octo@example.com",
|
|
Token: encryptedToken,
|
|
}).Error; err != nil {
|
|
t.Fatalf("failed to create controller session: %v", err)
|
|
}
|
|
|
|
if err := db.Create(&models.GitHubAppInstallState{
|
|
UserID: user.ID,
|
|
State: "install-state",
|
|
ExpiresAt: time.Now().Add(10 * time.Minute),
|
|
}).Error; err != nil {
|
|
t.Fatalf("failed to create install state: %v", err)
|
|
}
|
|
|
|
controller := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/github/app/installations/999/verify" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusForbidden)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": "installation_not_accessible"})
|
|
}))
|
|
defer controller.Close()
|
|
withControlServiceBaseURL(t, controller.URL)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/github/app/callback?state=install-state&installation_id=999&setup_action=install", nil)
|
|
rec := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(rec)
|
|
ctx.Request = req
|
|
|
|
GitHubAppInstallCallback(ctx)
|
|
|
|
if rec.Code != http.StatusTemporaryRedirect {
|
|
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
location := rec.Header().Get("Location")
|
|
if !strings.Contains(location, "github_app_error=installation_not_accessible") {
|
|
t.Fatalf("unexpected redirect location: %s", location)
|
|
}
|
|
}
|