Ctrl K
ComunidadGuías…

faynoSync_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"bytes"
	"compress/gzip"
	"context"
	"crypto/md5"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
	"time"

	"faynoSync/mongod"
	"faynoSync/redisdb"
	"faynoSync/server/handler"
	"faynoSync/server/handler/info"
	"faynoSync/server/model"
	"faynoSync/server/utils"
	"faynoSync/server/utils/storage"

	faynosync "github.com/ku9nov/faynosync-sdk-go"

	"github.com/beevik/etree"
	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis/v8"
	"github.com/sirupsen/logrus"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
)

var (
	client        *mongo.Client
	appDB         mongod.AppRepository
	mongoDatabase *mongo.Database
	configDB      connstring.ConnString
	apiKey        string
	apiUrl        string
	s3Endpoint    string
	s3Bucket      string
	redisClient   *redis.Client
)

func TestMain(m *testing.M) {
	// Set up resources before running tests
	setup()
	// Run the tests
	code := m.Run()
	teardown()
	os.Exit(code)
}

func checkFileContent(t *testing.T, url string) string {
	client := &http.Client{
		Timeout: 10 * time.Second,
	}
	// GCS serves public objects from Google's edge cache for max-age, so an overwritten feed reads stale without a unique query.
	if viper.GetString("STORAGE_DRIVER") == "gcp" {
		url = fmt.Sprintf("%s?nocache=%d", url, time.Now().UnixNano())
	}
	resp, err := client.Get(url)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	// Read the response body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}

	// Log the content of the file
	logrus.Infof("Content of %s:\n%s", url, string(body))
	return string(body)
}

func copyFile(src, dst string) {
	input, err := os.ReadFile(src)
	if err != nil {
		logrus.Errorf("Failed to read the file: %v", err)
		return
	}

	err = os.WriteFile(dst, input, 0644)
	if err != nil {
		logrus.Errorf("Failed to copy the file: %v", err)
		return
	}
}

func generateFile(filename, content string) {
	err := os.WriteFile(filename, []byte(content), 0644)
	if err != nil {
		logrus.Errorf("Failed to generate the file %s: %v", filename, err)
		return
	}
}

func removeFile(filename string) {
	err := os.Remove(filename)
	if err != nil {
		logrus.Errorf("Failed to remove the file: %v", err)
		return
	}
}

func assertSDKMatchesScenario(t *testing.T, expected map[string]interface{}, actual *faynosync.UpdateResponse) {
	t.Helper()

	require.Equal(t, expected["update_available"], actual.UpdateAvailable)
	require.Equal(t, expected["critical"], actual.Critical)
	if expectedIntermediate, ok := expected["is_intermediate_required"]; ok {
		require.Equal(t, expectedIntermediate, actual.IsIntermediateRequired)
	}
	if expectedRollback, ok := expected["possible_rollback"]; ok {
		rollbackField := reflect.ValueOf(actual).Elem().FieldByName("PossibleRollback")
		require.True(t, rollbackField.IsValid(), "expected field PossibleRollback to exist when possible_rollback is provided")
		require.Equal(t, reflect.Bool, rollbackField.Kind(), "expected field PossibleRollback to be bool when possible_rollback is provided")
		require.Equal(t, expectedRollback, rollbackField.Bool())
	}

	if expectedChangelog, ok := expected["changelog"]; ok {
		require.Equal(t, expectedChangelog, actual.Changelog)
	}

	if expectedURL, ok := expected["update_url"]; ok {
		require.Equal(t, expectedURL, actual.UpdateURL)
	}

	actualPackages := make(map[string]string, len(actual.PackageURLs))
	for _, pkg := range actual.PackageURLs {
		actualPackages[pkg.Package] = pkg.URL
	}

	for key, expectedValue := range expected {
		if !strings.HasPrefix(key, "update_url_") {
			continue
		}

		pkg := strings.TrimPrefix(key, "update_url_")
		require.Equal(t, expectedValue, actualPackages[pkg], "package URL mismatch for %s", pkg)
	}
}

func setup() {
	viper.SetConfigType("env")
	viper.SetConfigName(".env")
	// set the configuration file path
	viper.AddConfigPath(".")
	// read in the configuration file
	// Same as the API: the file is optional, environment variables (e.g. from Compose) take precedence
	if err := viper.ReadInConfig(); err != nil {
		logrus.Infoln(".env file not found, using system variables")
	}
	viper.AutomaticEnv()
	// Create a single database connection
	s3Bucket = viper.GetString("S3_BUCKET_NAME")
	s3Endpoint = viper.GetString("S3_ENDPOINT")
	if viper.GetString("STORAGE_DRIVER") == "digitalocean" {
		s3Endpoint = fmt.Sprintf("https://%s.%s", s3Bucket, s3Endpoint)
	}
	if viper.GetString("STORAGE_DRIVER") == "gcp" {
		s3Endpoint = fmt.Sprintf("https://storage.googleapis.com/%s", s3Bucket)
	}
	apiUrl = viper.GetString("API_URL")
	client, configDB = mongod.ConnectToDatabase(viper.GetString("MONGODB_URL_TESTS"))
	if err := mongod.RunMigrationsUp(client, configDB.Database); err != nil {
		panic(err)
	}
	appDB = mongod.NewAppRepository(&configDB, client)
	mongoDatabase = client.Database(configDB.Database)
	redisConfig := redisdb.RedisConfig{
		Addr:     viper.GetString("REDIS_HOST") + ":" + viper.GetString("REDIS_PORT"),
		Password: viper.GetString("REDIS_PASSWORD"),
		DB:       viper.GetInt("REDIS_DB"),
	}
	redisClient = redisdb.ConnectToRedis(redisConfig)
	os.Setenv("API_KEY", viper.GetString("API_KEY"))
	apiKey = viper.GetString("API_KEY")
	copyFile("LICENSE", "testapp.dmg")
	copyFile("LICENSE", "testapp.pkg")
	generateFile("RELEASES", "RELEASES FILE FOR WINDOWS sqirrel")
	generateFile("latest.yml", "latest file for windows electron-builder")
	generateFile("latest-mac.yml", "latest file for macos electron builder")
	copyFile("LICENSE", "test.zip")
	copyFile("LICENSE", "test.exe")

}

func teardown() {
	if redisClient != nil {
		err := redisClient.FlushDB(context.Background()).Err()
		if err != nil {
			logrus.Errorf("Failed to flush Redis DB: %v", err)
		} else {
			logrus.Infoln("Redis DB flushed successfully.")
		}
	}

	adminsCollection := mongoDatabase.Collection("admins")
	filter := bson.M{"username": bson.M{"$in": []string{"admin", "administrator"}}}

	// Delete the admin user from the collection
	_, err := adminsCollection.DeleteMany(context.TODO(), filter)
	if err != nil {
		logrus.Errorf("Failed to remove admin users: %v", err)
	}
	logrus.Infoln("Successfully removed admin users.")
	client.Disconnect(context.Background())
	logrus.Infoln("MongoDB is disconnected.")
	removeFile("testapp.dmg")
	removeFile("testapp.pkg")
	removeFile("RELEASES")
	removeFile("latest.yml")
	removeFile("latest-mac.yml")
	removeFile("test.zip")
	removeFile("test.exe")
}

func generateDateRangeAndStats(startDate time.Time, days int) ([]interface{}, []interface{}) {
	var dateRange []interface{}
	var dailyStats []interface{}

	for i := 0; i < days; i++ {
		d := startDate.AddDate(0, 0, i).Format("2006-01-02")
		dateRange = append(dateRange, d)
		dailyStats = append(dailyStats, d)
	}
	return dateRange, dailyStats
}

func TestHealthCheck(t *testing.T) {

	router := gin.Default()
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/health", func(c *gin.Context) {
		handler.HealthCheck(c)
	})

	req, _ := http.NewRequest("GET", "/health", nil)

	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	// Check the response body.
	expected := `{"status":"healthy"}`
	assert.Equal(t, expected, w.Body.String())
}

func TestAuthRateLimits(t *testing.T) {
	require.NotNil(t, redisClient)
	ctx := context.Background()

	// Fixed windows reset on the minute; start early in one so no subtest straddles a boundary.
	if s := time.Now().Second(); s > 40 {
		time.Sleep(time.Duration(61-s) * time.Second)
	}

	const rlUser = "ratelimitadmin"
	const rlPassword = "ratelimit-password"

	resetLimits := func() {
		keys, err := redisClient.Keys(ctx, "auth:rl:*").Result()
		require.NoError(t, err)
		if len(keys) > 0 {
			require.NoError(t, redisClient.Del(ctx, keys...).Err())
		}
	}
	cleanup := func() {
		resetLimits()
		_, err := mongoDatabase.Collection("admins").DeleteOne(ctx, bson.M{"username": rlUser})
		require.NoError(t, err)
	}
	cleanup()
	t.Cleanup(cleanup)

	h := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router := gin.Default()
	router.POST("/signup", h.SignUp)
	router.POST("/login", h.Login)

	send := func(path, ip, body string) *httptest.ResponseRecorder {
		req := httptest.NewRequest(http.MethodPost, path, bytes.NewBufferString(body))
		req.Header.Set("Content-Type", "application/json")
		req.RemoteAddr = ip + ":1234"
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)
		return w
	}
	login := func(ip, username, password string) *httptest.ResponseRecorder {
		return send("/login", ip, fmt.Sprintf(`{"username":%q,"password":%q}`, username, password))
	}
	signup := func(ip, username, key string) *httptest.ResponseRecorder {
		return send("/signup", ip, fmt.Sprintf(`{"username":%q,"password":%q,"api_key":%q}`, username, rlPassword, key))
	}
	assertLimited := func(t *testing.T, w *httptest.ResponseRecorder, maxRetryAfter int) {
		t.Helper()
		require.Equal(t, http.StatusTooManyRequests, w.Code, w.Body.String())
		var retryAfter int
		_, err := fmt.Sscan(w.Header().Get("Retry-After"), &retryAfter)
		require.NoError(t, err)
		require.True(t, retryAfter > 0 && retryAfter <= maxRetryAfter, "Retry-After %d", retryAfter)
		assert.JSONEq(t, `{"error":"too many attempts, try again later"}`, w.Body.String())
	}

	t.Run("signup and login succeed under the limits", func(t *testing.T) {
		resetLimits()
		require.Equal(t, http.StatusOK, signup("10.1.0.1", rlUser, apiKey).Code)
		w := login("10.1.0.1", rlUser, rlPassword)
		require.Equal(t, http.StatusOK, w.Code, w.Body.String())
		assert.Contains(t, w.Body.String(), "token")
	})

	t.Run("successful login resets the per-username counter", func(t *testing.T) {
		resetLimits()
		for i := 0; i < 4; i++ {
			require.Equal(t, http.StatusUnauthorized, login(fmt.Sprintf("10.2.0.%d", i), rlUser, "wrong").Code)
		}
		require.Equal(t, http.StatusOK, login("10.2.1.1", rlUser, rlPassword).Code)
		for i := 0; i < 5; i++ {
			require.Equal(t, http.StatusUnauthorized, login(fmt.Sprintf("10.2.2.%d", i), rlUser, "wrong").Code)
		}
		assertLimited(t, login("10.2.3.1", rlUser, "wrong"), 60)
		assertLimited(t, login("10.2.3.2", rlUser, rlPassword), 60)
		require.Equal(t, http.StatusUnauthorized, login("10.2.3.3", "another-user", "wrong").Code)
	})

	t.Run("unknown username is limited like an existing one", func(t *testing.T) {
		resetLimits()
		for i := 0; i < 5; i++ {
			require.Equal(t, http.StatusUnauthorized, login(fmt.Sprintf("10.3.0.%d", i), "no-such-user", "wrong").Code)
		}
		assertLimited(t, login("10.3.1.1", "no-such-user", "wrong"), 60)
	})

	t.Run("per-IP login limit", func(t *testing.T) {
		resetLimits()
		for i := 0; i < 10; i++ {
			require.Equal(t, http.StatusUnauthorized, login("10.4.0.1", fmt.Sprintf("user-%d", i), "wrong").Code)
		}
		assertLimited(t, login("10.4.0.1", rlUser, rlPassword), 60)
		require.Equal(t, http.StatusOK, login("10.4.0.2", rlUser, rlPassword).Code)
	})

	t.Run("per-IP signup limit", func(t *testing.T) {
		resetLimits()
		for i := 0; i < 5; i++ {
			require.Equal(t, http.StatusUnauthorized, signup("10.5.0.1", rlUser, "wrong").Code)
		}
		assertLimited(t, signup("10.5.0.1", rlUser, apiKey), 3600)
		require.Equal(t, http.StatusUnauthorized, signup("10.5.0.2", rlUser, "wrong").Code)
	})

	t.Run("global signup limit", func(t *testing.T) {
		resetLimits()
		for i := 0; i < 20; i++ {
			require.Equal(t, http.StatusUnauthorized, signup(fmt.Sprintf("10.6.0.%d", i), rlUser, "wrong").Code)
		}
		assertLimited(t, signup("10.6.1.1", rlUser, "wrong"), 3600)
	})
}

func TestFailedSignUp(t *testing.T) {
	router := gin.Default()
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/signup", func(c *gin.Context) {
		handler.SignUp(c)
	})

	payload := `{"username": "admin", "password": "password", "api_key": "UHp3aKb40poKZluZByWQ"}`
	req, err := http.NewRequest("POST", "/signup", bytes.NewBufferString(payload))
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusUnauthorized, w.Code)

	// Parse the JSON response body to extract the token.
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"error":"wrong api key"}`
	assert.Equal(t, expected, w.Body.String())
}

func TestSignUp(t *testing.T) {
	router := gin.Default()
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/signup", func(c *gin.Context) {
		handler.SignUp(c)
	})

	regKey := os.Getenv("API_KEY")
	payload := fmt.Sprintf(`{"username": "admin", "password": "password", "api_key": "%s"}`, regKey)
	req, err := http.NewRequest("POST", "/signup", bytes.NewBufferString(payload))
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	// Parse the JSON response body to extract the token.
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"result":"Successfully created admin user."}`
	assert.Equal(t, expected, w.Body.String())
}

func TestSignUpSecondUser(t *testing.T) {
	router := gin.Default()
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/signup", func(c *gin.Context) {
		handler.SignUp(c)
	})

	regKey := os.Getenv("API_KEY")
	payload := fmt.Sprintf(`{"username": "administrator", "password": "password", "api_key": "%s"}`, regKey)
	req, err := http.NewRequest("POST", "/signup", bytes.NewBufferString(payload))
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	// Parse the JSON response body to extract the token.
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"result":"Successfully created admin user."}`
	assert.Equal(t, expected, w.Body.String())
}

func TestFailedLogin(t *testing.T) {

	router := gin.Default()
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/login", func(c *gin.Context) {
		handler.Login(c)
	})

	// Create a JSON payload for the request
	payload := `{"username": "admin", "password": "password1"}`

	req, err := http.NewRequest("POST", "/login", bytes.NewBufferString(payload))
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Content-Type", "application/json")
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusUnauthorized, w.Code)

	// Parse the JSON response body to extract the token.
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"error":"invalid username or password"}`
	assert.Equal(t, expected, w.Body.String())
}

var authToken string

func TestLogin(t *testing.T) {

	router := gin.Default()
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/login", func(c *gin.Context) {
		handler.Login(c)
	})

	// Create a JSON payload for the request
	payload := `{"username": "admin", "password": "password"}`

	req, err := http.NewRequest("POST", "/login", bytes.NewBufferString(payload))
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Content-Type", "application/json")
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	// Parse the JSON response body to extract the token.
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check that the "token" key exists in the response.
	token, tokenExists := response["token"]
	assert.True(t, tokenExists)

	var ok bool
	authToken, ok = token.(string)
	require.True(t, ok, w.Body.String())

	// Check that the authToken variable has been set (assuming authToken is a global variable).
	assert.NotEmpty(t, authToken)
}

var authTokenSecondUser string

func TestLoginSecondUser(t *testing.T) {

	router := gin.Default()
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/login", func(c *gin.Context) {
		handler.Login(c)
	})

	// Create a JSON payload for the request
	payload := `{"username": "administrator", "password": "password"}`

	req, err := http.NewRequest("POST", "/login", bytes.NewBufferString(payload))
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Content-Type", "application/json")
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	// Parse the JSON response body to extract the token.
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check that the "token" key exists in the response.
	token, tokenExists := response["token"]
	assert.True(t, tokenExists)

	var ok bool
	authTokenSecondUser, ok = token.(string)
	require.True(t, ok, w.Body.String())

	// Check that the authTokenSecondUser variable has been set (assuming authTokenSecondUser is a global variable).
	assert.NotEmpty(t, authTokenSecondUser)
}

func TestListApps(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /app/list endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/app/list", func(c *gin.Context) {
		handler.ListApps(c)
	})

	// Create a POST request for the /app/list endpoint.
	req, err := http.NewRequest("GET", "/app/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"apps":null}`
	assert.Equal(t, expected, w.Body.String())
}

func TestListAppsWithInvalidToken(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/app/list", func(c *gin.Context) {
		handler.ListApps(c)
	})

	tests := []struct {
		name      string
		modifyFn  func(token string) string
		expectErr bool
	}{
		{
			name: "Modified header",
			modifyFn: func(token string) string {
				parts := strings.Split(token, ".")
				parts[0] = parts[0][:len(parts[0])-1] + "X" // Modify header
				return strings.Join(parts, ".")
			},
			expectErr: true,
		},
		{
			name: "Modified payload",
			modifyFn: func(token string) string {
				parts := strings.Split(token, ".")
				parts[1] = parts[1][:len(parts[1])-1] + "Y" // Modify payload
				return strings.Join(parts, ".")
			},
			expectErr: true,
		},
		{
			name: "Modified signature",
			modifyFn: func(token string) string {
				parts := strings.Split(token, ".")
				parts[2] = parts[2][:len(parts[2])-1] + "Z" // Modify signature
				return strings.Join(parts, ".")
			},
			expectErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			modifiedToken := tt.modifyFn(authToken)

			logrus.Infof("Testing with token: %s", modifiedToken)

			req, err := http.NewRequest("GET", "/app/list", nil)
			if err != nil {
				t.Fatal(err)
			}

			req.Header.Set("Authorization", "Bearer "+modifiedToken)

			router.ServeHTTP(w, req)

			logrus.Infof("Status code for %s: %d", tt.name, w.Code)
			logrus.Infof("Response body: %s", w.Body.String())

			if tt.expectErr {
				assert.Equal(t, http.StatusUnauthorized, w.Code)
			} else {
				assert.Equal(t, http.StatusOK, w.Code)
			}
		})
	}
}

var idTestappApp string
var idPublicTestappApp string

func TestAppCreate(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /app/create route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/app/create", utils.CheckPermission(utils.PermissionCreate, utils.ResourceApps, mongoDatabase), func(c *gin.Context) {
		handler.CreateApp(c)
	})

	// Create multipart/form-data request body
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	// Add a field for the channel to the form
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := `{"app": "testapp", "private": "true", "tuf": "true", "reports": "true"}`
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form data
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request to the /app/create endpoint
	req, err := http.NewRequest("POST", "/app/create", body)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Content-Type header for multipart/form-data
	req.Header.Set("Content-Type", writer.FormDataContentType())
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check for the presence of the "createAppResult.Created" key in the response
	id, idExists := response["createAppResult.Created"]
	assert.True(t, idExists)
	idTestappApp = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, idTestappApp)
}

var keyValueReportKeyTestapp string

func TestListReportKeys(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/report-keys/list", utils.CheckPermission(utils.PermissionEdit, utils.ResourceApps, mongoDatabase), func(c *gin.Context) {
		handler.ListReportKeys(c)
	})

	req, err := http.NewRequest("GET", "/report-keys/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Authorization", "Bearer "+authToken)
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	type ReportKeyResponse struct {
		ReportKeys []model.ReportKeyListItem `json:"report_keys"`
	}
	var actual ReportKeyResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}
	if !assert.Len(t, actual.ReportKeys, 1) {
		return
	}
	item := actual.ReportKeys[0]
	assert.Equal(t, "testapp", item.AppName)
	assert.True(t, strings.HasPrefix(item.KeyValue, utils.ReportKeyPrefix))
	assert.Len(t, item.KeyValue, len(utils.ReportKeyPrefix)+64)
	keyValueReportKeyTestapp = item.KeyValue
}

func TestRegenerateReportKey(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/report-keys/regenerate", utils.CheckPermission(utils.PermissionEdit, utils.ResourceApps, mongoDatabase), func(c *gin.Context) {
		handler.RegenerateReportKey(c)
	})

	payload := `{"app_id": "` + idTestappApp + `"}`
	req, err := http.NewRequest("POST", "/report-keys/regenerate", bytes.NewBufferString(payload))
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Authorization", "Bearer "+authToken)
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}
	assert.Equal(t, idTestappApp, response["app_id"])
	assert.NotEqual(t, keyValueReportKeyTestapp, response["key_value"].(string))
	assert.True(t, strings.HasPrefix(response["key_value"].(string), utils.ReportKeyPrefix))
	assert.Len(t, response["key_value"].(string), len(utils.ReportKeyPrefix)+64)
	keyValueReportKeyTestapp = response["key_value"].(string)
}
func TestFailedRegenerateReportKeyWithSecondaryUser(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/report-keys/regenerate", utils.CheckPermission(utils.PermissionEdit, utils.ResourceApps, mongoDatabase), func(c *gin.Context) {
		handler.RegenerateReportKey(c)
	})

	payload := `{"app_id": "` + idTestappApp + `"}`
	req, err := http.NewRequest("POST", "/report-keys/regenerate", bytes.NewBufferString(payload))
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	req.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusNotFound, w.Code)
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}
	assert.Equal(t, "app_name not found in apps_meta collection", response["error"].(string))
}
func TestListReportKeysWithSecondaryUser(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/report-keys/list", utils.CheckPermission(utils.PermissionEdit, utils.ResourceApps, mongoDatabase), func(c *gin.Context) {
		handler.ListReportKeys(c)
	})

	req, err := http.NewRequest("GET", "/report-keys/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	type ReportKeyResponse struct {
		ReportKeys []model.ReportKeyListItem `json:"report_keys"`
	}
	var actual ReportKeyResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}
	if !assert.Len(t, actual.ReportKeys, 0) {
		t.Fatal("Expected 0 report keys")
	}
}

func TestCreatePublicApp(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /app/create route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/app/create", func(c *gin.Context) {
		handler.CreateApp(c)
	})

	// Create multipart/form-data request body
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	// Add a field for the channel to the form
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := `{"app": "public testapp", "reports": "true", "cdn": "true"}`
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form data
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request to the /app/create endpoint
	req, err := http.NewRequest("POST", "/app/create", body)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Content-Type header for multipart/form-data
	req.Header.Set("Content-Type", writer.FormDataContentType())
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check for the presence of the "createAppResult.Created" key in the response
	idPublic, idExists := response["createAppResult.Created"]
	assert.True(t, idExists)
	idPublicTestappApp = idPublic.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, idTestappApp)
}

var idTestappAppWithUpdaters string

func TestCreateAppWithUpdaters(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /app/create route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/app/create", func(c *gin.Context) {
		handler.CreateApp(c)
	})

	// Create multipart/form-data request body
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	// Add a field for the channel to the form
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := `{"app": "updaters"}`
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form data
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request to the /app/create endpoint
	req, err := http.NewRequest("POST", "/app/create", body)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Content-Type header for multipart/form-data
	req.Header.Set("Content-Type", writer.FormDataContentType())
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check for the presence of the "createAppResult.Created" key in the response
	id, idExists := response["createAppResult.Created"]
	assert.True(t, idExists)
	idTestappAppWithUpdaters = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, idTestappAppWithUpdaters)
}

func TestSecondaryAppCreate(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /app/create route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/app/create", func(c *gin.Context) {
		handler.CreateApp(c)
	})

	// Create multipart/form-data request body
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	// Add a field for the channel to the form
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := `{"app": "testapp"}`
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form data
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request to the /app/create endpoint
	req, err := http.NewRequest("POST", "/app/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for multipart/form-data
	req.Header.Set("Content-Type", writer.FormDataContentType())

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 500).
	assert.Equal(t, http.StatusInternalServerError, w.Code)

	// Check the response body for the desired error message.
	expectedErrorMessage := `{"error":"app with this name already exists"}`
	assert.Equal(t, expectedErrorMessage, w.Body.String())
}

var uploadedFirstApp string

func TestUpload(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /upload endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/upload", func(c *gin.Context) {
		handler.UploadApp(c)
	})

	// Create a file to upload (you can replace this with a test file path).
	filePath := "LICENSE"
	file, err := os.Open(filePath)
	if err != nil {
		t.Fatal(err)
	}
	defer file.Close()

	// Create a multipart/form-data request with the file.
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		t.Fatal(err)
	}
	_, err = io.Copy(part, file)
	if err != nil {
		t.Fatal(err)
	}
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := `{"app_name": "testapp", "version": "0.0.1.137"}`
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request for the /upload endpoint.
	req, err := http.NewRequest("POST", "/upload", body)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Content-Type header for multipart/form-data.
	req.Header.Set("Content-Type", writer.FormDataContentType())

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}
	logrus.Infof("Response Body after unmarshaling: %+v", response)
	// Check that the "token" key exists in the response.
	id, idExists := response["uploadResult.Uploaded"]
	assert.True(t, idExists)

	uploadedFirstApp = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, uploadedFirstApp)
}

func TestUploadDuplicateApp(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /upload endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/upload", func(c *gin.Context) {
		handler.UploadApp(c)
	})

	// Create a file to upload (you can replace this with a test file path).
	filePath := "LICENSE"
	file, err := os.Open(filePath)
	if err != nil {
		t.Fatal(err)
	}
	defer file.Close()

	// Create a multipart/form-data request with the file.
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		t.Fatal(err)
	}
	_, err = io.Copy(part, file)
	if err != nil {
		t.Fatal(err)
	}
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := `{"app_name": "testapp", "version": "0.0.1.137"}`
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request for the /upload endpoint.
	req, err := http.NewRequest("POST", "/upload", body)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Content-Type header for multipart/form-data.
	req.Header.Set("Content-Type", writer.FormDataContentType())

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code (expecting 409).
	assert.Equal(t, http.StatusConflict, w.Code)

	// Check the response body for the desired error message.
	expectedErrorMessage := `{"error":"app with this name, version, platform, architecture and extension already exists"}`
	assert.Equal(t, expectedErrorMessage, w.Body.String())
}

func TestDeleteApp(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /apps/delete endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.DELETE("/apps/delete", func(c *gin.Context) {
		handler.DeleteSpecificVersionOfApp(c)
	})

	// Create a POST request for the /apps/delete endpoint.
	req, err := http.NewRequest("DELETE", "/apps/delete?id="+uploadedFirstApp, nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"deleteSpecificAppResult.DeletedCount":1}`
	assert.Equal(t, expected, w.Body.String())
}

func TestListChannels(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /channel/list endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/channel/list", func(c *gin.Context) {
		handler.ListChannels(c)
	})

	// Create a POST request for the /channel/list endpoint.
	req, err := http.NewRequest("GET", "/channel/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"channels":null}`
	assert.Equal(t, expected, w.Body.String())
}

var idNightlyChannel string
var idStableChannel string

func TestChannelCreateNightly(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /channel/create route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/channel/create", func(c *gin.Context) {
		handler.CreateChannel(c)
	})

	payload := `{
		"channel": "nightly"
	}`
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /channel/create endpoint
	req, err := http.NewRequest("POST", "/channel/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check for the presence of the "createChannelResult.Created" key in the response
	id, idExists := response["createChannelResult.Created"]
	assert.True(t, idExists)
	idNightlyChannel = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, idNightlyChannel)
}
func TestChannelCreateWithWrongName(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /channel/create route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/channel/create", func(c *gin.Context) {
		handler.CreateChannel(c)
	})

	payload := `{
		"channel": "nightly *"
	}`

	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /channel/create endpoint
	req, err := http.NewRequest("POST", "/channel/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 400).
	assert.Equal(t, http.StatusBadRequest, w.Code)

	// Check the response body for the desired error message.
	expectedErrorMessage := `{"error":"invalid channel name"}`
	assert.Equal(t, expectedErrorMessage, w.Body.String())
}
func TestSecondaryChannelCreateNightly(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /channel/create route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/channel/create", func(c *gin.Context) {
		handler.CreateChannel(c)
	})

	payload := `{
		"channel": "nightly"
	}`

	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /channel/create endpoint
	req, err := http.NewRequest("POST", "/channel/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 500).
	assert.Equal(t, http.StatusInternalServerError, w.Code)

	// Check the response body for the desired error message.
	expectedErrorMessage := `{"error":"channel with this name already exists"}`
	assert.Equal(t, expectedErrorMessage, w.Body.String())
}

func TestChannelCreateStable(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /channel/create route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/channel/create", func(c *gin.Context) {
		handler.CreateChannel(c)
	})

	payload := `{
		"channel": "stable"
	}`
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /channel/create endpoint
	req, err := http.NewRequest("POST", "/channel/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check for the presence of the "createChannelResult.Created" key in the response
	id, idExists := response["createChannelResult.Created"]
	assert.True(t, idExists)
	idStableChannel = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, idStableChannel)
}

func TestUploadAppWithoutChannel(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /upload endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/upload", func(c *gin.Context) {
		handler.UploadApp(c)
	})

	// Create a file to upload (you can replace this with a test file path).
	filePath := "LICENSE"
	file, err := os.Open(filePath)
	if err != nil {
		t.Fatal(err)
	}
	defer file.Close()

	// Create a multipart/form-data request with the file.
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		t.Fatal(err)
	}
	_, err = io.Copy(part, file)
	if err != nil {
		t.Fatal(err)
	}
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := `{"app_name": "testapp", "version": "0.0.1.137"}`
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request for the upload endpoint.
	req, err := http.NewRequest("POST", "/upload", body)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Content-Type header for multipart/form-data.
	req.Header.Set("Content-Type", writer.FormDataContentType())

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code (expecting 500).
	assert.Equal(t, http.StatusBadRequest, w.Code)

	// Check the response body for the desired error message.
	expectedErrorMessage := `{"error":"you have a created channels, setting channel is required"}`
	assert.Equal(t, expectedErrorMessage, w.Body.String())
}

func TestListPlatforms(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /platform/list endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/platform/list", func(c *gin.Context) {
		handler.ListPlatforms(c)
	})

	// Create a POST request for the /platform/list endpoint.
	req, err := http.NewRequest("GET", "/platform/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"platforms":null}`
	assert.Equal(t, expected, w.Body.String())
}

var platformId string

func TestPlatformCreate(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/create", func(c *gin.Context) {
		handler.CreatePlatform(c)
	})

	payload := `{
		"platform": "universalPlatform"
	}`
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/create endpoint
	req, err := http.NewRequest("POST", "/platform/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}
	id, idExists := response["createPlatformResult.Created"]
	assert.True(t, idExists)
	platformId = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, platformId)
}

var platformIdWindows string

func TestPlatformCreateWindows(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/create", func(c *gin.Context) {
		handler.CreatePlatform(c)
	})

	payload := `{
		"platform": "windows"
	}`
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/create endpoint
	req, err := http.NewRequest("POST", "/platform/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}
	id, idExists := response["createPlatformResult.Created"]
	assert.True(t, idExists)
	platformIdWindows = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, platformIdWindows)
}

func TestUpdatePlatformWindows(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /platform/update route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/update", func(c *gin.Context) {
		handler.UpdatePlatform(c)
	})

	payload := fmt.Sprintf(`{
		"id": "%s", 
		"platform":"windows",
		"updaters": [
			{ "type": "manual", "default": false },
			{ "type": "squirrel_windows", "default": true },
			{ "type": "electron-builder", "default": false }
		]
	}`, platformIdWindows)
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/update endpoint
	req, err := http.NewRequest("POST", "/platform/update", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check for the presence of the "updateChannelResult.Updated" key in the response
	updated, exists := response["updatePlatformResult.Updated"]
	assert.True(t, exists)
	assert.True(t, updated.(bool))
}

var platformIdMacos string

func TestPlatformCreateMacos(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/create", func(c *gin.Context) {
		handler.CreatePlatform(c)
	})

	payload := `{
		"platform": "macos"
	}`
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/create endpoint
	req, err := http.NewRequest("POST", "/platform/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}
	id, idExists := response["createPlatformResult.Created"]
	assert.True(t, idExists)
	platformIdMacos = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, platformIdMacos)
}

var platformIdMacosTauri string

func TestPlatformCreateMacosTauri(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/create", func(c *gin.Context) {
		handler.CreatePlatform(c)
	})

	payload := `{
		"platform": "macosTauri",
		"updaters": [
			{ "type": "manual", "default": false },
			{ "type": "tauri", "default": true }
		]
	}`
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/create endpoint
	req, err := http.NewRequest("POST", "/platform/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}
	id, idExists := response["createPlatformResult.Created"]
	assert.True(t, idExists)
	platformIdMacosTauri = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, platformIdMacosTauri)
}

func TestUpdatePlatformMacos(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /platform/update route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/update", func(c *gin.Context) {
		handler.UpdatePlatform(c)
	})

	payload := fmt.Sprintf(`{
		"id": "%s", 
		"platform":"macos",
		"updaters": [
			{ "type": "manual", "default": false },
			{ "type": "squirrel_darwin", "default": true },
			{ "type": "electron-builder", "default": false },
			{ "type": "tauri", "default": false }
		]
	}`, platformIdMacos)
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/update endpoint
	req, err := http.NewRequest("POST", "/platform/update", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check for the presence of the "updateChannelResult.Updated" key in the response
	updated, exists := response["updatePlatformResult.Updated"]
	assert.True(t, exists)
	assert.True(t, updated.(bool))
}

var platformIdMacosSquirrel string

func TestPlatformCreateMacosSquirrel(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/create", func(c *gin.Context) {
		handler.CreatePlatform(c)
	})

	payload := `{
		"platform": "macosSquirrel"
	}`
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/create endpoint
	req, err := http.NewRequest("POST", "/platform/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}
	id, idExists := response["createPlatformResult.Created"]
	assert.True(t, idExists)
	platformIdMacosSquirrel = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, platformIdMacosSquirrel)
}

func TestUpdatePlatformMacosSquirrel(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /platform/update route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/update", func(c *gin.Context) {
		handler.UpdatePlatform(c)
	})

	payload := fmt.Sprintf(`{
		"id": "%s", 
		"platform":"macosSquirrel",
		"updaters": [
			{ "type": "manual", "default": false },
			{ "type": "squirrel_darwin", "default": true },
			{ "type": "electron-builder", "default": false }
		]
	}`, platformIdMacosSquirrel)
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/update endpoint
	req, err := http.NewRequest("POST", "/platform/update", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	// Check for the presence of the "updateChannelResult.Updated" key in the response
	updated, exists := response["updatePlatformResult.Updated"]
	assert.True(t, exists)
	assert.True(t, updated.(bool))
}

func TestSecondaryPlatformCreate(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/create", func(c *gin.Context) {
		handler.CreatePlatform(c)
	})

	payload := `{
		"platform": "universalPlatform"
	}`

	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/create endpoint
	req, err := http.NewRequest("POST", "/platform/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 500).
	assert.Equal(t, http.StatusInternalServerError, w.Code)

	// Check the response body for the desired error message.
	expectedErrorMessage := `{"error":"platform with this name already exists"}`
	assert.Equal(t, expectedErrorMessage, w.Body.String())
}

var secondPlatformId string

func TestCreateSecondPlatform(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/create", func(c *gin.Context) {
		handler.CreatePlatform(c)
	})

	payload := `{
		"platform": "secondPlatform"
	}`
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/create endpoint
	req, err := http.NewRequest("POST", "/platform/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}
	id, idExists := response["createPlatformResult.Created"]
	assert.True(t, idExists)
	secondPlatformId = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, secondPlatformId)
}

func TestUploadAppWithoutPlatform(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /upload endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/upload", func(c *gin.Context) {
		handler.UploadApp(c)
	})

	// Create a file to upload (you can replace this with a test file path).
	filePath := "LICENSE"
	file, err := os.Open(filePath)
	if err != nil {
		t.Fatal(err)
	}
	defer file.Close()

	// Create a multipart/form-data request with the file.
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		t.Fatal(err)
	}
	_, err = io.Copy(part, file)
	if err != nil {
		t.Fatal(err)
	}
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := `{"app_name": "testapp", "version": "0.0.1.137", "channel": "nightly"}`
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request for the upload endpoint.
	req, err := http.NewRequest("POST", "/upload", body)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Content-Type header for multipart/form-data.
	req.Header.Set("Content-Type", writer.FormDataContentType())

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code (expecting 500).
	assert.Equal(t, http.StatusBadRequest, w.Code)

	// Check the response body for the desired error message.
	expectedErrorMessage := `{"error":"you have a created platforms, setting platform is required"}`
	assert.Equal(t, expectedErrorMessage, w.Body.String())
}

func TestListArchs(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /arch/list endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/arch/list", func(c *gin.Context) {
		handler.ListArchs(c)
	})

	// Create a POST request for the /arch/list endpoint.
	req, err := http.NewRequest("GET", "/arch/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"archs":null}`
	assert.Equal(t, expected, w.Body.String())
}

var archId string

func TestArchCreate(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/arch/create", func(c *gin.Context) {
		handler.CreateArch(c)
	})

	payload := `{
		"arch": "universalArch"
	}`
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /channel/create endpoint
	req, err := http.NewRequest("POST", "/arch/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	id, idExists := response["createArchResult.Created"]
	assert.True(t, idExists)
	archId = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, archId)
}
func TestSecondaryArchCreate(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/arch/create", func(c *gin.Context) {
		handler.CreateArch(c)
	})

	payload := `{
		"arch": "universalArch"
	}`

	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /channel/create endpoint
	req, err := http.NewRequest("POST", "/arch/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 500).
	assert.Equal(t, http.StatusInternalServerError, w.Code)

	// Check the response body for the desired error message.
	expectedErrorMessage := `{"error":"arch with this name already exists"}`
	assert.Equal(t, expectedErrorMessage, w.Body.String())
}

var secondArchId string

func TestCreateSecondArch(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/arch/create", func(c *gin.Context) {
		handler.CreateArch(c)
	})

	payload := `{
		"arch": "secondArch"
	}`

	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /channel/create endpoint
	req, err := http.NewRequest("POST", "/arch/create", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status %d; got %d", http.StatusOK, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	id, idExists := response["createArchResult.Created"]
	assert.True(t, idExists)
	secondArchId = id.(string)
	assert.True(t, idExists)
	assert.NotEmpty(t, secondArchId)
}

func TestUploadAppWithoutArch(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /upload endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/upload", func(c *gin.Context) {
		handler.UploadApp(c)
	})

	// Create a file to upload (you can replace this with a test file path).
	filePath := "LICENSE"
	file, err := os.Open(filePath)
	if err != nil {
		t.Fatal(err)
	}
	defer file.Close()

	// Create a multipart/form-data request with the file.
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		t.Fatal(err)
	}
	_, err = io.Copy(part, file)
	if err != nil {
		t.Fatal(err)
	}
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := `{"app_name": "testapp", "version": "0.0.1.137", "channel": "nightly", "platform": "universalPlatform"}`
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request for the upload endpoint.
	req, err := http.NewRequest("POST", "/upload", body)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Content-Type header for multipart/form-data.
	req.Header.Set("Content-Type", writer.FormDataContentType())

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code (expecting 500).
	assert.Equal(t, http.StatusBadRequest, w.Code)

	// Check the response body for the desired error message.
	expectedErrorMessage := `{"error":"you have a created archs, setting arch is required"}`
	assert.Equal(t, expectedErrorMessage, w.Body.String())
}

var uploadedAppIDs []string

func TestMultipleUpload(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	// Define the route for the upload endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/upload", func(c *gin.Context) {
		handler.UploadApp(c)
	})

	// Create a file to upload (you can replace this with a test file path).
	filePaths := []string{"testapp.dmg", "testapp.pkg", "LICENSE"}
	for _, filePath := range filePaths {
		file, err := os.Open(filePath)
		if err != nil {
			t.Fatal(err)
		}
		defer file.Close()

		combinations := []struct {
			AppName     string
			AppVersion  string
			ChannelName string
			Published   bool
			Critical    bool
			Platform    string
			Arch        string
		}{
			{"public testapp", "0.0.1.137", "nightly", true, false, "universalPlatform", "universalArch"},
			{"testapp", "0.0.2.137", "nightly", true, false, "universalPlatform", "universalArch"},
			{"testapp", "0.0.3.137", "nightly", false, false, "universalPlatform", "universalArch"},
			{"testapp", "0.0.4.137", "stable", true, true, "universalPlatform", "universalArch"},
			{"testapp", "0.0.5.137", "stable", false, false, "universalPlatform", "universalArch"},
		}

		// Iterate through the combinations and upload the file for each combination.
		for _, combo := range combinations {
			w := httptest.NewRecorder()
			// Reset the request body for each iteration.
			body := &bytes.Buffer{}
			writer := multipart.NewWriter(body)
			part, err := writer.CreateFormFile("file", filepath.Base(filePath))
			if err != nil {
				t.Fatal(err)
			}
			_, err = io.Copy(part, file)
			if err != nil {
				t.Fatal(err)
			}
			dataPart, err := writer.CreateFormField("data")
			if err != nil {
				t.Fatal(err)
			}
			payload := fmt.Sprintf(`{"app_name": "%s", "version": "%s", "channel": "%s", "publish": %v, "critical": %v, "platform": "%s", "arch": "%s"}`, combo.AppName, combo.AppVersion, combo.ChannelName, combo.Published, combo.Critical, combo.Platform, combo.Arch)
			_, err = dataPart.Write([]byte(payload))
			if err != nil {
				t.Fatal(err)
			}

			// Close the writer to finalize the form
			err = writer.Close()
			if err != nil {
				t.Fatal(err)
			}
			// Create a POST request for the /upload endpoint with the current combination.
			req, err := http.NewRequest("POST", "/upload", body)
			if err != nil {
				t.Fatal(err)
			}

			// Set the Content-Type header for multipart/form-data.
			req.Header.Set("Content-Type", writer.FormDataContentType())

			// Set the Authorization header.
			req.Header.Set("Authorization", "Bearer "+authToken)
			// Serve the request using the Gin router.
			router.ServeHTTP(w, req)

			// Check the response status code.
			assert.Equal(t, http.StatusOK, w.Code)
			var response map[string]interface{}
			err = json.Unmarshal(w.Body.Bytes(), &response)
			if err != nil {
				t.Fatal(err)
			}

			id, idExists := response["uploadResult.Uploaded"]
			assert.True(t, idExists)

			// Check if the id already exists in the uploadedAppIDs array
			exists := false
			for _, val := range uploadedAppIDs {
				if val == id {
					exists = true
					break
				}
			}

			// If id does not exist in the array, append it
			if !exists {
				uploadedAppIDs = append(uploadedAppIDs, id.(string))
			}

			assert.True(t, idExists)
			assert.NotEmpty(t, id.(string))
		}
	}
}

var uploadedAppIDsWithUpdaters []string

func TestMultipleUploadWithUpdaters(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	// Define the route for the upload endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/upload", func(c *gin.Context) {
		handler.UploadApp(c)
	})

	combinations := []struct {
		AppName     string
		AppVersion  string
		ChannelName string
		Published   bool
		Critical    bool
		Platform    string
		Arch        string
		Updater     string
		FileName    string
		Signature   string
	}{
		{"updaters", "0.0.1.137", "nightly", false, false, "macosSquirrel", "universalArch", "squirrel_darwin", "testapp.dmg", ""},
		{"updaters", "0.0.2.137", "nightly", true, false, "macosSquirrel", "universalArch", "squirrel_darwin", "test.zip", ""},
		{"updaters", "0.0.1.137", "nightly", false, false, "macosTauri", "universalArch", "tauri", "test.zip", ""},
		{"updaters", "0.0.2.137", "nightly", true, false, "macosTauri", "universalArch", "tauri", "test.zip", "dW50cnVzdGVkU2lnbmF0dXJlVGhhdExvb2tzTGlrZUFsb25nU3RyaW5n=="},
		{"updaters", "0.0.3.137", "nightly", false, false, "macos", "universalArch", "electron-builder", "testapp.dmg", ""},
		{"updaters", "0.0.4.137", "nightly", true, false, "macos", "universalArch", "electron-builder", "latest-mac.yml", ""},
		{"updaters", "0.0.5.137", "stable", false, true, "windows", "universalArch", "squirrel_windows", "test.exe", ""},
		{"updaters", "0.0.6.137", "stable", true, false, "windows", "universalArch", "squirrel_windows", "RELEASES", ""},
		{"updaters", "0.0.7.137", "stable", false, false, "windows", "universalArch", "electron-builder", "test.exe", ""},
		{"updaters", "0.0.8.137", "stable", true, false, "windows", "universalArch", "electron-builder", "latest.yml", ""},
	}

	// Iterate through the combinations and upload the file for each combination.
	for _, combo := range combinations {
		logrus.Infoln("Uploading this combo:", combo)
		filePath := combo.FileName
		file, err := os.Open(filePath)

		if err != nil {
			t.Fatal(err)
		}

		defer file.Close()
		w := httptest.NewRecorder()
		// Reset the request body for each iteration.
		body := &bytes.Buffer{}
		writer := multipart.NewWriter(body)
		part, err := writer.CreateFormFile("file", filepath.Base(filePath))
		if err != nil {
			t.Fatal(err)
		}
		_, err = io.Copy(part, file)
		if err != nil {
			t.Fatal(err)
		}
		dataPart, err := writer.CreateFormField("data")
		if err != nil {
			t.Fatal(err)
		}
		payload := fmt.Sprintf(`{"app_name": "%s", "version": "%s", "channel": "%s", "publish": %v, "critical": %v, "platform": "%s", "arch": "%s", "updater": "%s", "signature": "%s"}`, combo.AppName, combo.AppVersion, combo.ChannelName, combo.Published, combo.Critical, combo.Platform, combo.Arch, combo.Updater, combo.Signature)
		logrus.Infoln("Payload: ", payload)
		_, err = dataPart.Write([]byte(payload))
		if err != nil {
			t.Fatal(err)
		}

		// Close the writer to finalize the form
		err = writer.Close()
		if err != nil {
			t.Fatal(err)
		}
		// Create a POST request for the /upload endpoint with the current combination.
		req, err := http.NewRequest("POST", "/upload", body)
		if err != nil {
			t.Fatal(err)
		}

		// Set the Content-Type header for multipart/form-data.
		req.Header.Set("Content-Type", writer.FormDataContentType())

		// Set the Authorization header.
		req.Header.Set("Authorization", "Bearer "+authToken)
		// Serve the request using the Gin router.
		router.ServeHTTP(w, req)

		// Check if this combination should expect an error
		shouldExpectError := (combo.AppVersion == "0.0.1.137" && combo.FileName == "testapp.dmg") ||
			(combo.AppVersion == "0.0.3.137" && combo.FileName == "testapp.dmg") ||
			(combo.AppVersion == "0.0.5.137" && combo.FileName == "test.exe") ||
			(combo.AppVersion == "0.0.7.137" && combo.FileName == "test.exe") ||
			(combo.AppVersion == "0.0.1.137" && combo.FileName == "test.zip")

		if shouldExpectError {
			// Expect 400 status code for error cases
			assert.Equal(t, http.StatusBadRequest, w.Code)
			var response map[string]interface{}
			err = json.Unmarshal(w.Body.Bytes(), &response)
			if err != nil {
				t.Fatal(err)
			}
			// Check that there's an error message in the response
			errorMsg, hasError := response["error"]
			assert.True(t, hasError, "Expected error message in response")
			assert.NotEmpty(t, errorMsg, "Error message should not be empty")

			// Check specific error messages based on updater type
			var expectedErrorMsg string
			switch combo.Updater {
			case "squirrel_windows":
				expectedErrorMsg = "squirrel windows updater requires a RELEASES file for update functionality. Please include a RELEASES file in your upload"
			case "electron-builder":
				expectedErrorMsg = "electron-builder updater requires a YML/YAML file for update functionality. Please include a .yml or .yaml file in your upload"
			case "squirrel_darwin":
				expectedErrorMsg = "squirrel darwin updater requires a ZIP archive for update functionality. Please include a ZIP file in your upload"
			case "tauri":
				expectedErrorMsg = "tauri updater requires a signature parameter for update functionality. Please include a signature in your request"
			}

			if expectedErrorMsg != "" {
				assert.Equal(t, expectedErrorMsg, errorMsg, "Expected specific error message for %s updater", combo.Updater)
			}
		} else {
			// Check the response status code for successful cases
			assert.Equal(t, http.StatusOK, w.Code)
			var response map[string]interface{}
			err = json.Unmarshal(w.Body.Bytes(), &response)
			if err != nil {
				t.Fatal(err)
			}

			id, idExists := response["uploadResult.Uploaded"]
			assert.True(t, idExists)

			// Check if the id already exists in the uploadedAppIDsWithUpdaters array
			exists := false
			for _, val := range uploadedAppIDsWithUpdaters {
				if val == id {
					exists = true
					break
				}
			}

			// If id does not exist in the array, append it
			if !exists {
				uploadedAppIDsWithUpdaters = append(uploadedAppIDsWithUpdaters, id.(string))
			}

			assert.True(t, idExists)
			assert.NotEmpty(t, id.(string))
		}
	}

}

func TestCheckVersionWithUpdaters(t *testing.T) {
	router := gin.Default()
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/checkVersion", func(c *gin.Context) {
		handler.FindLatestVersion(c)
	})
	// Define test scenarios.
	testScenarios := []struct {
		AppName      string
		Version      string
		ChannelName  string
		ExpectedJSON map[string]interface{}
		ExpectedCode int
		Published    bool
		Platform     string
		Arch         string
		TestName     string
		Owner        string
		Updater      string
		ExpectedBody string
	}{
		{
			AppName:      "updaters",
			Version:      "0.0.2.137",
			ChannelName:  "nightly",
			ExpectedJSON: map[string]interface{}{"status": "no_content"},
			ExpectedCode: http.StatusNoContent,
			Platform:     "macosSquirrel",
			Arch:         "universalArch",
			TestName:     "SquirrelDarwinUpdateNotAvailable",
			Owner:        "admin",
			Updater:      "squirrel_darwin",
		},
		{
			AppName:     "updaters",
			Version:     "0.0.1.135",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				"critical":         false,
				"update_available": true,
				"url":              fmt.Sprintf("%s/%s", s3Endpoint, "updaters-admin/nightly/macosSquirrel/universalArch/updaters-0.0.2.137.zip"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "macosSquirrel",
			Arch:         "universalArch",
			TestName:     "SquirrelDarwinUpdateAvailable",
			Owner:        "admin",
			Updater:      "squirrel_darwin",
		},
		{
			AppName:      "updaters",
			Version:      "0.0.2.137",
			ChannelName:  "nightly",
			ExpectedJSON: map[string]interface{}{"status": "no_content"},
			ExpectedCode: http.StatusNoContent,
			Platform:     "macosTauri",
			Arch:         "universalArch",
			TestName:     "TauriUpdateNotAvailable",
			Owner:        "admin",
			Updater:      "tauri",
		},
		{
			AppName:     "updaters",
			Version:     "0.0.1.135",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				"signature": "dW50cnVzdGVkU2lnbmF0dXJlVGhhdExvb2tzTGlrZUFsb25nU3RyaW5n==",
				"version":   "0.0.2.137",
				"url":       fmt.Sprintf("%s/%s", s3Endpoint, "updaters-admin/nightly/macosTauri/universalArch/updaters-0.0.2.137.zip"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "macosTauri",
			Arch:         "universalArch",
			TestName:     "TauriUpdateAvailable",
			Owner:        "admin",
			Updater:      "tauri",
		},
		{
			AppName:      "updaters",
			Version:      "0.0.4.137",
			ChannelName:  "nightly",
			ExpectedJSON: map[string]interface{}{"status": "no_content"},
			ExpectedCode: http.StatusNoContent,
			// Published:    false,
			Platform: "macos",
			Arch:     "universalArch",
			TestName: "ElectronBuilderMacosUpdateNotAvailable",
			Owner:    "admin",
			Updater:  "electron-builder",
		},
		{
			AppName:     "updaters",
			Version:     "0.0.1.133",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				"critical":         false,
				"update_available": true,
				"update_url_yml":   fmt.Sprintf("%s/%s", s3Endpoint, "electron-builder/updaters-admin/0.0.4.137/nightly/macos/universalArch/latest-mac.yml"),
			},
			ExpectedCode: http.StatusFound,
			// Published:    false,
			Platform:     "macos",
			Arch:         "universalArch",
			TestName:     "ElectronBuilderMacosUpdateAvailable",
			Owner:        "admin",
			Updater:      "electron-builder",
			ExpectedBody: "latest file for macos electron builder",
		},
		{
			AppName:      "updaters",
			Version:      "0.0.8.137",
			ChannelName:  "stable",
			ExpectedJSON: map[string]interface{}{"status": "no_content"},
			ExpectedCode: http.StatusNoContent,
			// Published:    false,
			Platform: "windows",
			Arch:     "universalArch",
			TestName: "ElectronBuilderWindowsUpdateNotAvailable",
			Owner:    "admin",
			Updater:  "electron-builder",
		},
		{
			AppName:     "updaters",
			Version:     "0.0.1.131",
			ChannelName: "stable",
			ExpectedJSON: map[string]interface{}{
				"status": "redirect",
				"url":    fmt.Sprintf("%s/%s", s3Endpoint, "electron-builder/updaters-admin/0.0.8.137/stable/windows/universalArch/latest.yml"),
			},
			ExpectedCode: http.StatusFound,
			// Published:    false,
			Platform:     "windows",
			Arch:         "universalArch",
			TestName:     "ElectronBuilderWindowsUpdateAvailable",
			Owner:        "admin",
			Updater:      "electron-builder",
			ExpectedBody: "latest file for windows electron-builder",
		},
	}

	for _, scenario := range testScenarios {
		t.Run(scenario.TestName, func(t *testing.T) {
			w := httptest.NewRecorder()

			// Create a GET request for checking the version.
			req, err := http.NewRequest("GET", fmt.Sprintf("/checkVersion?app_name=%s&version=%s&channel=%s&platform=%s&arch=%s&owner=%s&updater=%s", scenario.AppName, scenario.Version, scenario.ChannelName, scenario.Platform, scenario.Arch, scenario.Owner, scenario.Updater), nil)
			if err != nil {
				t.Fatal(err)
			}

			// Serve the request using the Gin router.
			router.ServeHTTP(w, req)

			// Check the response status code.
			assert.Equal(t, scenario.ExpectedCode, w.Code)
			fmt.Println("Code: , Response: ", w.Code, w.Body.String())

			switch scenario.ExpectedCode {
			case http.StatusNoContent:
				// For 204, we expect either empty body or {"status": "no_content"}
				if len(w.Body.Bytes()) == 0 {
					// Empty body is fine for 204
					return
				}
				fmt.Println("Content: ", w.Body.String())
				// If there's content, it should be {"status": "no_content"}
				var actual map[string]interface{}
				err = json.Unmarshal(w.Body.Bytes(), &actual)
				if err != nil {
					t.Fatal(err)
				}
				expectedNoContent := map[string]interface{}{"status": "no_content"}
				assert.Equal(t, expectedNoContent, actual)
			case http.StatusFound:
				// For 302 Found (redirect), check the Location header and HTML body
				locationHeader := w.Header().Get("Location")
				logrus.Infof("Location Header: %s", locationHeader)
				if locationHeader != "" {
					// If Location header is set, that's the redirect URL
					var expectedURL string
					if url, exists := scenario.ExpectedJSON["url"]; exists {
						expectedURL = url.(string)
					} else if updateURL, exists := scenario.ExpectedJSON["update_url_yml"]; exists {
						expectedURL = updateURL.(string)
					} else {
						t.Fatal("Neither 'url' nor 'update_url_yml' found in ExpectedJSON")
					}
					assert.Equal(t, expectedURL, locationHeader)
					body := checkFileContent(t, locationHeader)
					logrus.Infof("asserting that body: %s, contains: %s", body, scenario.ExpectedBody)
					assert.Contains(t, body, scenario.ExpectedBody)
				} else {
					// If no Location header, check HTML body for redirect link
					body := w.Body.String()
					expectedURL := scenario.ExpectedJSON["url"].(string)
					// Check if the expected URL is present in the HTML body
					assert.Contains(t, body, expectedURL)
				}
			default:
				// For other status codes, parse and compare JSON
				var actual map[string]interface{}
				logrus.Infoln("Body: ", w.Body.String())
				err = json.Unmarshal(w.Body.Bytes(), &actual)
				if err != nil {
					t.Fatal(err)
				}

				// Compare the response with the expected values.
				assert.Equal(t, scenario.ExpectedJSON, actual)
			}
		})
	}
}

// ============================================================================
// Sparkle & Velopack full interactive flow (self-contained block).
// Two dedicated apps, 5 versions each, mixed publish/critical/changelog across
// two platforms, publish/unpublish toggles, and materialized-feed verification
// on S3 — including per-platform/per-channel isolation (regression for the
// multi-platform materialization bug). Cleans up its own app + version docs.
// Reuses channels (nightly/stable), platforms (macos/macosSquirrel/windows) and
// arch (universalArch) created earlier in the ordered suite.
// ============================================================================

type updaterFeedFile struct {
	name    string
	content []byte
}

func updaterFlowRouter() *gin.Engine {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	h := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/app/create", func(c *gin.Context) { h.CreateApp(c) })
	router.POST("/upload", func(c *gin.Context) { h.UploadApp(c) })
	router.POST("/upload/init", func(c *gin.Context) { h.InitPresignedUpload(c) })
	router.POST("/upload/complete", func(c *gin.Context) { h.CompletePresignedUpload(c) })
	router.POST("/apps/update", func(c *gin.Context) { h.UpdateSpecificApp(c) })
	router.DELETE("/apps/delete", func(c *gin.Context) { h.DeleteSpecificVersionOfApp(c) })
	router.DELETE("/app/delete", func(c *gin.Context) { h.DeleteApp(c) })
	return router
}

func doUpdaterRequest(t *testing.T, router *gin.Engine, path, payload string, files []updaterFeedFile) *httptest.ResponseRecorder {
	t.Helper()
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	for _, f := range files {
		part, err := writer.CreateFormFile("file", f.name)
		require.NoError(t, err)
		_, err = part.Write(f.content)
		require.NoError(t, err)
	}
	dataPart, err := writer.CreateFormField("data")
	require.NoError(t, err)
	_, err = dataPart.Write([]byte(payload))
	require.NoError(t, err)
	require.NoError(t, writer.Close())
	req, err := http.NewRequest("POST", path, body)
	require.NoError(t, err)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	req.Header.Set("Authorization", "Bearer "+authToken)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	return w
}

// cleanupUpdaterApp removes the app through the delete API, so the feed files and
// artifacts it materialized in storage are erased the same way a real delete does.
func cleanupUpdaterApp(t *testing.T, appName string) {
	t.Helper()
	ctx := context.Background()
	var meta struct {
		ID primitive.ObjectID `bson:"_id"`
	}
	if err := mongoDatabase.Collection("apps_meta").FindOne(ctx, bson.M{"app_name": appName, "owner": "admin"}).Decode(&meta); err != nil {
		return
	}

	cursor, err := mongoDatabase.Collection("apps").Find(ctx, bson.M{"app_id": meta.ID})
	require.NoError(t, err)
	var versions []struct {
		ID primitive.ObjectID `bson:"_id"`
	}
	require.NoError(t, cursor.All(ctx, &versions))

	router := updaterFlowRouter()
	for _, v := range versions {
		w := doUpdaterDelete(t, router, "/apps/delete?id="+v.ID.Hex())
		require.Equal(t, http.StatusOK, w.Code, "delete version %s: %s", v.ID.Hex(), w.Body.String())
	}

	w := doUpdaterDelete(t, router, "/app/delete?id="+meta.ID.Hex())
	require.Equal(t, http.StatusOK, w.Code, "delete app %s: %s", appName, w.Body.String())
}

func doUpdaterDelete(t *testing.T, router *gin.Engine, path string) *httptest.ResponseRecorder {
	t.Helper()
	req, err := http.NewRequest("DELETE", path, nil)
	require.NoError(t, err)
	req.Header.Set("Authorization", "Bearer "+authToken)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	return w
}

func createUpdaterApp(t *testing.T, router *gin.Engine, appName string) {
	t.Helper()
	w := doUpdaterRequest(t, router, "/app/create", fmt.Sprintf(`{"app": "%s"}`, appName), nil)
	require.Equal(t, http.StatusOK, w.Code, "create %s: %s", appName, w.Body.String())
}

func parseAppcastItems(t *testing.T, feed string) map[string]*etree.Element {
	t.Helper()
	doc := etree.NewDocument()
	require.NoError(t, doc.ReadFromString(feed), "feed is not valid xml:\n%s", feed)
	out := map[string]*etree.Element{}
	for _, it := range doc.FindElements("//item") {
		if v := it.SelectElement("sparkle:version"); v != nil {
			out[strings.TrimSpace(v.Text())] = it
		}
	}
	return out
}

func TestSparkleUpdaterFlow(t *testing.T) {
	cleanupUpdaterApp(t, "sparkleapp")
	defer cleanupUpdaterApp(t, "sparkleapp")
	router := updaterFlowRouter()
	createUpdaterApp(t, router, "sparkleapp")

	appcast := func(zip, sv string) []byte {
		return []byte(fmt.Sprintf(`<?xml version="1.0" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0"><channel><title>sparkleapp</title>
<item><title>%s</title><sparkle:version>%s</sparkle:version><sparkle:shortVersionString>%s</sparkle:shortVersionString>
<enclosure url="http://orig/%s" length="100" type="application/octet-stream" sparkle:edSignature="SIG=="/></item></channel></rss>`, sv, sv, sv, zip))
	}

	versions := []struct {
		version, channel, platform, sparkleVer, changelog string
		publish, critical                                 bool
	}{
		{"0.0.1", "nightly", "macos", "1", "Initial release", true, false},
		{"0.0.2", "nightly", "macos", "2", "Security fix critical", true, true},
		{"0.0.3", "nightly", "macos", "3", "WIP not ready", false, false},
		{"0.0.4", "stable", "macos", "4", "Stable build", true, false},
		{"0.0.5", "nightly", "macosSquirrel", "5", "Other platform build", true, false},
	}
	ids := map[string]string{}
	for _, v := range versions {
		zip := fmt.Sprintf("sparkleapp-%s.zip", v.version)
		files := []updaterFeedFile{
			{name: "appcast." + v.channel + ".xml", content: appcast(zip, v.sparkleVer)},
			{name: zip, content: []byte("fake zip payload")},
		}
		payload := fmt.Sprintf(`{"app_name":"sparkleapp","version":"%s","channel":"%s","publish":%v,"critical":%v,"platform":"%s","arch":"universalArch","updater":"sparkle","changelog":"%s"}`,
			v.version, v.channel, v.publish, v.critical, v.platform, v.changelog)
		w := doUpdaterRequest(t, router, "/upload", payload, files)
		require.Equal(t, http.StatusOK, w.Code, "upload %s: %s", v.version, w.Body.String())
		var resp map[string]interface{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
		id, _ := resp["uploadResult.Uploaded"].(string)
		require.NotEmpty(t, id, "no upload id for %s", v.version)
		ids[v.version] = id
	}

	nightlyMacURL := fmt.Sprintf("%s/sparkle/admin/sparkleapp/macos/universalArch/appcast.nightly.xml", s3Endpoint)

	t.Run("nightly_macos_feed", func(t *testing.T) {
		items := parseAppcastItems(t, checkFileContent(t, nightlyMacURL))
		require.Contains(t, items, "1", "published v0.0.1 must be in feed")
		require.Contains(t, items, "2", "published v0.0.2 must be in feed")
		require.NotContains(t, items, "3", "unpublished v0.0.3 must be excluded")
		require.NotContains(t, items, "5", "macosSquirrel v0.0.5 must not leak into macos feed")
		assert.Nil(t, items["1"].SelectElement("sparkle:criticalUpdate"), "non-critical v0.0.1 must have no criticalUpdate")
		assert.NotNil(t, items["2"].SelectElement("sparkle:criticalUpdate"), "critical v0.0.2 must carry criticalUpdate")
	})

	t.Run("changelog_rendered", func(t *testing.T) {
		feed := checkFileContent(t, nightlyMacURL)
		assert.Contains(t, feed, "Initial release")
		assert.Contains(t, feed, "Security fix critical")
	})

	t.Run("channel_and_platform_isolation", func(t *testing.T) {
		stable := parseAppcastItems(t, checkFileContent(t, fmt.Sprintf("%s/sparkle/admin/sparkleapp/macos/universalArch/appcast.stable.xml", s3Endpoint)))
		require.Contains(t, stable, "4")
		require.NotContains(t, stable, "1", "nightly build must not appear in stable feed")

		other := parseAppcastItems(t, checkFileContent(t, fmt.Sprintf("%s/sparkle/admin/sparkleapp/macosSquirrel/universalArch/appcast.nightly.xml", s3Endpoint)))
		require.Contains(t, other, "5")
		require.NotContains(t, other, "1", "macos build must not appear in macosSquirrel feed")
		require.NotContains(t, other, "2")
	})

	toggle := func(version, channel, platform string, publish bool) {
		payload := fmt.Sprintf(`{"id":"%s","app_name":"sparkleapp","version":"%s","channel":"%s","publish":%v,"platform":"%s","arch":"universalArch"}`,
			ids[version], version, channel, publish, platform)
		w := doUpdaterRequest(t, router, "/apps/update", payload, nil)
		require.Equal(t, http.StatusOK, w.Code, "toggle %s publish=%v: %s", version, publish, w.Body.String())
	}

	t.Run("unpublish_removes_from_feed", func(t *testing.T) {
		toggle("0.0.2", "nightly", "macos", false)
		items := parseAppcastItems(t, checkFileContent(t, nightlyMacURL))
		require.NotContains(t, items, "2", "unpublished v0.0.2 must disappear from feed")
		require.Contains(t, items, "1")
	})

	t.Run("publish_adds_to_feed", func(t *testing.T) {
		toggle("0.0.3", "nightly", "macos", true)
		items := parseAppcastItems(t, checkFileContent(t, nightlyMacURL))
		require.Contains(t, items, "3", "newly published v0.0.3 must appear in feed")
	})

	// Regression for targeted regeneration: a single version with artifacts on
	// TWO platforms. A version-level unpublish must clear it from BOTH platform
	// feeds — proving targeted regen covers every tuple the version spans, not
	// just the one in the request payload.
	t.Run("multiplatform_version_unpublish_clears_all_feeds", func(t *testing.T) {
		sqURL := fmt.Sprintf("%s/sparkle/admin/sparkleapp/macosSquirrel/universalArch/appcast.nightly.xml", s3Endpoint)

		zipMac := "sparkleapp-0.0.9.zip"
		wUp := doUpdaterRequest(t, router, "/upload",
			`{"app_name":"sparkleapp","version":"0.0.9","channel":"nightly","publish":true,"critical":false,"platform":"macos","arch":"universalArch","updater":"sparkle","changelog":"multi platform"}`,
			[]updaterFeedFile{{name: "appcast.nightly.xml", content: appcast(zipMac, "9")}, {name: zipMac, content: []byte("z")}})
		require.Equal(t, http.StatusOK, wUp.Code, wUp.Body.String())
		var upResp map[string]interface{}
		require.NoError(t, json.Unmarshal(wUp.Body.Bytes(), &upResp))
		id, _ := upResp["uploadResult.Uploaded"].(string)
		require.NotEmpty(t, id)

		// add a second platform (macosSquirrel) to the SAME version via update
		zipSq := "sparkleapp-0.0.9-sq.zip"
		wAdd := doUpdaterRequest(t, router, "/apps/update",
			fmt.Sprintf(`{"id":"%s","app_name":"sparkleapp","version":"0.0.9","channel":"nightly","publish":true,"critical":false,"platform":"macosSquirrel","arch":"universalArch","updater":"sparkle","changelog":"multi platform"}`, id),
			[]updaterFeedFile{{name: "appcast.nightly.xml", content: appcast(zipSq, "9")}, {name: zipSq, content: []byte("z")}})
		require.Equal(t, http.StatusOK, wAdd.Code, wAdd.Body.String())

		require.Contains(t, parseAppcastItems(t, checkFileContent(t, nightlyMacURL)), "9", "v0.0.9 must be in macos feed")
		require.Contains(t, parseAppcastItems(t, checkFileContent(t, sqURL)), "9", "v0.0.9 must be in macosSquirrel feed")

		// version-level unpublish (payload names only macos) must still clear BOTH
		wUnpub := doUpdaterRequest(t, router, "/apps/update",
			fmt.Sprintf(`{"id":"%s","app_name":"sparkleapp","version":"0.0.9","channel":"nightly","publish":false,"platform":"macos","arch":"universalArch"}`, id),
			nil)
		require.Equal(t, http.StatusOK, wUnpub.Code, wUnpub.Body.String())
		require.NotContains(t, parseAppcastItems(t, checkFileContent(t, nightlyMacURL)), "9", "unpublish must remove v0.0.9 from macos feed")
		require.NotContains(t, parseAppcastItems(t, checkFileContent(t, sqURL)), "9", "unpublish must remove v0.0.9 from macosSquirrel feed (all tuples)")
	})
}

type velopackFeedAsset struct {
	Version   string
	Type      string
	FileName  string
	NotesHTML string
}

type velopackFeedDoc struct {
	Assets []velopackFeedAsset
}

func parseVelopackFeed(t *testing.T, feed string) map[string]velopackFeedAsset {
	t.Helper()
	var doc velopackFeedDoc
	require.NoError(t, json.Unmarshal([]byte(feed), &doc), "feed is not valid json:\n%s", feed)
	out := map[string]velopackFeedAsset{}
	for _, a := range doc.Assets {
		if a.Type == "Full" {
			out[a.Version] = a
		}
	}
	return out
}

func TestVelopackUpdaterFlow(t *testing.T) {
	cleanupUpdaterApp(t, "velopackapp")
	defer cleanupUpdaterApp(t, "velopackapp")
	router := updaterFlowRouter()
	createUpdaterApp(t, router, "velopackapp")

	releases := func(nupkg string) []byte {
		return []byte(fmt.Sprintf(`{"Assets":[{"Type":"Full","FileName":"%s","SHA1":"a","SHA256":"b","Size":100}]}`, nupkg))
	}

	versions := []struct {
		version, channel, platform, changelog string
		publish                               bool
	}{
		{"1.0.0", "nightly", "windows", "First release", true},
		{"1.0.1", "nightly", "windows", "Second release notes", true},
		{"1.0.2", "nightly", "windows", "Unpublished wip", false},
		{"1.0.3", "stable", "windows", "Stable build", true},
		{"1.0.4", "nightly", "macos", "Mac build", true},
	}
	ids := map[string]string{}
	for _, v := range versions {
		nupkg := fmt.Sprintf("velopackapp-%s-full.nupkg", v.version)
		files := []updaterFeedFile{
			{name: "releases." + v.channel + ".json", content: releases(nupkg)},
			{name: nupkg, content: []byte("fake nupkg payload")},
		}
		payload := fmt.Sprintf(`{"app_name":"velopackapp","version":"%s","channel":"%s","publish":%v,"critical":false,"platform":"%s","arch":"universalArch","updater":"velopack","changelog":"%s"}`,
			v.version, v.channel, v.publish, v.platform, v.changelog)
		w := doUpdaterRequest(t, router, "/upload", payload, files)
		require.Equal(t, http.StatusOK, w.Code, "upload %s: %s", v.version, w.Body.String())
		var resp map[string]interface{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
		id, _ := resp["uploadResult.Uploaded"].(string)
		require.NotEmpty(t, id, "no upload id for %s", v.version)
		ids[v.version] = id
	}

	nightlyWinURL := fmt.Sprintf("%s/velopack/admin/velopackapp/windows/universalArch/releases.nightly.json", s3Endpoint)

	t.Run("nightly_windows_feed", func(t *testing.T) {
		assets := parseVelopackFeed(t, checkFileContent(t, nightlyWinURL))
		require.Contains(t, assets, "1.0.0", "published 1.0.0 must be in feed")
		require.Contains(t, assets, "1.0.1", "published 1.0.1 must be in feed")
		require.NotContains(t, assets, "1.0.2", "unpublished 1.0.2 must be excluded")
		require.NotContains(t, assets, "1.0.4", "macos 1.0.4 must not leak into windows feed")
		assert.Contains(t, assets["1.0.1"].NotesHTML, "Second release notes", "changelog must render into NotesHTML")
	})

	t.Run("channel_and_platform_isolation", func(t *testing.T) {
		stable := parseVelopackFeed(t, checkFileContent(t, fmt.Sprintf("%s/velopack/admin/velopackapp/windows/universalArch/releases.stable.json", s3Endpoint)))
		require.Contains(t, stable, "1.0.3")
		require.NotContains(t, stable, "1.0.0", "nightly build must not appear in stable feed")

		mac := parseVelopackFeed(t, checkFileContent(t, fmt.Sprintf("%s/velopack/admin/velopackapp/macos/universalArch/releases.nightly.json", s3Endpoint)))
		require.Contains(t, mac, "1.0.4")
		require.NotContains(t, mac, "1.0.0", "windows build must not appear in macos feed")
	})

	toggle := func(version, channel, platform string, publish bool) {
		payload := fmt.Sprintf(`{"id":"%s","app_name":"velopackapp","version":"%s","channel":"%s","publish":%v,"platform":"%s","arch":"universalArch"}`,
			ids[version], version, channel, publish, platform)
		w := doUpdaterRequest(t, router, "/apps/update", payload, nil)
		require.Equal(t, http.StatusOK, w.Code, "toggle %s publish=%v: %s", version, publish, w.Body.String())
	}

	t.Run("unpublish_removes_from_feed", func(t *testing.T) {
		toggle("1.0.0", "nightly", "windows", false)
		assets := parseVelopackFeed(t, checkFileContent(t, nightlyWinURL))
		require.NotContains(t, assets, "1.0.0", "unpublished 1.0.0 must disappear from feed")
		require.Contains(t, assets, "1.0.1")
	})

	t.Run("publish_adds_to_feed", func(t *testing.T) {
		toggle("1.0.2", "nightly", "windows", true)
		assets := parseVelopackFeed(t, checkFileContent(t, nightlyWinURL))
		require.Contains(t, assets, "1.0.2", "newly published 1.0.2 must appear in feed")
	})
}

// ============================================================================
// /apps/latest download links and the Tauri update URL (self-contained block).
// Regression for: updater-internal files (feeds, .nupkg, .blockmap, .sig, Tauri
// bundles) leaking into /apps/latest and overwriting each other by extension;
// the newest version being picked per channel instead of per platform/arch;
// staged rollouts reaching download pages; compound extensions stored as ".gz";
// and the Tauri /checkVersion url picked in random map order. Reuses channel
// nightly, platforms windows/macos/macosTauri and arch universalArch created
// earlier in the ordered suite. Cleans up its own apps.
// ============================================================================

func uploadLatestFlowVersion(t *testing.T, router *gin.Engine, appName, version, platform, updater, extra string, names ...string) string {
	t.Helper()
	files := make([]updaterFeedFile, 0, len(names))
	for _, name := range names {
		content := []byte(name)
		if name == "RELEASES" {
			content = []byte(fmt.Sprintf("AAAA %s-%s-full.nupkg 10\nBBBB %s-%s-delta.nupkg 5", appName, version, appName, version))
		}
		files = append(files, updaterFeedFile{name: name, content: content})
	}
	payload := fmt.Sprintf(`{"app_name":"%s","version":"%s","channel":"nightly","publish":true,"platform":"%s","arch":"universalArch","updater":"%s"%s}`,
		appName, version, platform, updater, extra)
	w := doUpdaterRequest(t, router, "/upload", payload, files)
	require.Equal(t, http.StatusOK, w.Code, "upload %s %s: %s", version, platform, w.Body.String())
	var resp map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	id, _ := resp["uploadResult.Uploaded"].(string)
	require.NotEmpty(t, id)
	return id
}

func latestDownloadPath(appName, query string) string {
	return fmt.Sprintf("/apps/latest?app_name=%s&channel=nightly&owner=admin&%s", appName, query)
}

func requireLatestRedirect(t *testing.T, appName, query, wantSuffix string) {
	t.Helper()
	w := serveVersionCheck(t, latestDownloadPath(appName, query), nil)
	require.Equal(t, http.StatusFound, w.Code, "%s: %s", query, w.Body.String())
	location := w.Header().Get("Location")
	require.True(t, strings.HasSuffix(location, wantSuffix), "%s redirected to %q, want suffix %q", query, location, wantSuffix)
}

func requireLatestNotFound(t *testing.T, appName, query string) {
	t.Helper()
	w := serveVersionCheck(t, latestDownloadPath(appName, query), nil)
	require.Equal(t, http.StatusNotFound, w.Code, "%s must not resolve: %s", query, w.Body.String())
}

// latestDownloadEntries returns package -> {url, version} per platform for arch universalArch.
func latestDownloadEntries(t *testing.T, appName, query string) map[string]map[string]map[string]string {
	t.Helper()
	w := serveVersionCheck(t, latestDownloadPath(appName, query), nil)
	require.Equal(t, http.StatusOK, w.Code, "%s: %s", query, w.Body.String())
	var body map[string]map[string]map[string]map[string]map[string]string
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body), w.Body.String())
	out := map[string]map[string]map[string]string{}
	for platform, archs := range body["nightly"] {
		out[platform] = archs["universalArch"]
	}
	return out
}

func requireLatestPackages(t *testing.T, entries map[string]map[string]string, version string, packages ...string) {
	t.Helper()
	got := make([]string, 0, len(entries))
	for pkg, entry := range entries {
		got = append(got, pkg)
		require.Equal(t, version, entry["version"], "package %s version", pkg)
	}
	require.ElementsMatch(t, packages, got, "entries: %v", entries)
}

func TestAppsLatestDownloadFlow(t *testing.T) {
	const app = "latestdlapp"
	cleanupUpdaterApp(t, app)
	defer cleanupUpdaterApp(t, app)
	router := updaterFlowRouter()
	createUpdaterApp(t, router, app)

	uploadLatestFlowVersion(t, router, app, "1.0.1", "windows", "squirrel_windows", "",
		"RELEASES", app+"-1.0.1-full.nupkg", app+"-1.0.1-delta.nupkg", "Setup.exe")
	uploadLatestFlowVersion(t, router, app, "1.0.1", "macos", "electron-builder", "",
		"latest-mac.yml", app+"-1.0.1-mac.zip", app+"-1.0.1-mac.zip.blockmap", app+"-1.0.1.dmg")

	t.Run("installers_only", func(t *testing.T) {
		requireLatestRedirect(t, app, "platform=windows&arch=universalArch", "/Setup.exe")
		requireLatestPackages(t, latestDownloadEntries(t, app, "platform=macos&arch=universalArch")["macos"], "1.0.1", "dmg", "zip")

		requireLatestNotFound(t, app, "platform=windows&arch=universalArch&package=nupkg")
		requireLatestNotFound(t, app, "platform=windows&arch=universalArch&package=no-extension")
		requireLatestNotFound(t, app, "platform=macos&arch=universalArch&package=yml")
		requireLatestNotFound(t, app, "platform=macos&arch=universalArch&package=blockmap")
	})

	t.Run("platform_missing_from_newest_release", func(t *testing.T) {
		uploadLatestFlowVersion(t, router, app, "1.0.2", "windows", "manual", "", app+"-1.0.2.exe")

		requireLatestRedirect(t, app, "platform=windows&arch=universalArch", app+"-1.0.2.exe")
		requireLatestRedirect(t, app, "platform=macos&arch=universalArch&package=dmg", app+"-1.0.1.dmg")

		all := latestDownloadEntries(t, app, "")
		requireLatestPackages(t, all["windows"], "1.0.2", "exe")
		requireLatestPackages(t, all["macos"], "1.0.1", "dmg", "zip")
	})

	t.Run("compound_extension_and_package_fallback", func(t *testing.T) {
		uploadLatestFlowVersion(t, router, app, "1.0.3", "macos", "manual", "", app+"-1.0.3-arm64.tar.gz")

		all := latestDownloadEntries(t, app, "")
		requireLatestPackages(t, all["macos"], "1.0.3", "tar.gz")
		require.True(t, strings.HasSuffix(all["macos"]["tar.gz"]["url"], app+"-1.0.3.tar.gz"), "stored as %s", all["macos"]["tar.gz"]["url"])
		requireLatestPackages(t, all["windows"], "1.0.2", "exe")

		requireLatestRedirect(t, app, "platform=macos&arch=universalArch", app+"-1.0.3.tar.gz")
		requireLatestRedirect(t, app, "platform=macos&arch=universalArch&package=tar.gz", app+"-1.0.3.tar.gz")
		requireLatestRedirect(t, app, "platform=macos&arch=universalArch&package=dmg", app+"-1.0.1.dmg")
		requireLatestRedirect(t, app, "platform=macos&arch=universalArch&package=zip", app+"-1.0.1-mac.zip")
		requireLatestNotFound(t, app, "platform=macos&arch=universalArch&package=gz")
	})

	t.Run("staged_rollout_skipped_until_full", func(t *testing.T) {
		id := uploadLatestFlowVersion(t, router, app, "1.0.4", "macos", "manual", `,"rollout":10`, app+"-1.0.4.dmg")

		requireLatestRedirect(t, app, "platform=macos&arch=universalArch&package=dmg", app+"-1.0.1.dmg")
		requireLatestRedirect(t, app, "platform=macos&arch=universalArch", app+"-1.0.3.tar.gz")
		requireLatestPackages(t, latestDownloadEntries(t, app, "")["macos"], "1.0.3", "tar.gz")

		w := doUpdaterRequest(t, router, "/apps/update",
			fmt.Sprintf(`{"id":"%s","app_name":"%s","version":"1.0.4","channel":"nightly","publish":true,"platform":"macos","arch":"universalArch","rollout":100}`, id, app), nil)
		require.Equal(t, http.StatusOK, w.Code, "rollout to 100: %s", w.Body.String())

		requireLatestRedirect(t, app, "platform=macos&arch=universalArch&package=dmg", app+"-1.0.4.dmg")
		requireLatestRedirect(t, app, "platform=macos&arch=universalArch", app+"-1.0.4.dmg")
		requireLatestPackages(t, latestDownloadEntries(t, app, "")["macos"], "1.0.4", "dmg")
	})
}

func TestTauriUpdateURLFlow(t *testing.T) {
	const app = "tauriurlapp"
	const signature = "dGF1cmktYnVuZGxlLXNpZ25hdHVyZQ=="
	cleanupUpdaterApp(t, app)
	defer cleanupUpdaterApp(t, app)
	router := updaterFlowRouter()
	createUpdaterApp(t, router, app)

	uploadLatestFlowVersion(t, router, app, "1.0.1", "macosTauri", "tauri", fmt.Sprintf(`,"signature":"%s"`, signature),
		app+".app.tar.gz", app+".app.tar.gz.sig", app+"_1.0.1_aarch64.dmg")

	// Every request uses another client version, so each one misses the response cache
	// and exercises the url pick again instead of replaying the first answer.
	t.Run("checkversion_returns_bundle_every_time", func(t *testing.T) {
		for i := 0; i < 25; i++ {
			path := fmt.Sprintf("/checkVersion?app_name=%s&version=0.0.0.%d&channel=nightly&platform=macosTauri&arch=universalArch&owner=admin&updater=tauri", app, i)
			w := serveVersionCheck(t, path, nil)
			require.Equal(t, http.StatusOK, w.Code, w.Body.String())
			var resp map[string]interface{}
			require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
			url, _ := resp["url"].(string)
			require.True(t, strings.HasSuffix(url, app+"-1.0.1.app.tar.gz"), "request %d got url %q", i, url)
			require.Equal(t, signature, resp["signature"])
			require.Equal(t, "1.0.1", resp["version"])
		}
	})

	t.Run("apps_latest_returns_installer", func(t *testing.T) {
		requireLatestRedirect(t, app, "platform=macosTauri&arch=universalArch", app+"-1.0.1.dmg")
		requireLatestNotFound(t, app, "platform=macosTauri&arch=universalArch&package=app.tar.gz")
		requireLatestNotFound(t, app, "platform=macosTauri&arch=universalArch&package=sig")
	})
}

func storageObjectStatus(t *testing.T, url string) int {
	t.Helper()
	if viper.GetString("STORAGE_DRIVER") == "gcp" {
		url = fmt.Sprintf("%s?nocache=%d", url, time.Now().UnixNano())
	}
	resp, err := (&http.Client{Timeout: 10 * time.Second}).Get(url)
	require.NoError(t, err)
	resp.Body.Close()
	return resp.StatusCode
}

func storedVersionPackages(t *testing.T, appName, version string) []string {
	t.Helper()
	ctx := context.Background()
	var meta struct {
		ID primitive.ObjectID `bson:"_id"`
	}
	require.NoError(t, mongoDatabase.Collection("apps_meta").FindOne(ctx, bson.M{"app_name": appName, "owner": "admin"}).Decode(&meta))
	var doc model.SpecificApp
	err := mongoDatabase.Collection("apps").FindOne(ctx, bson.M{"app_id": meta.ID, "version": version}).Decode(&doc)
	if err == mongo.ErrNoDocuments {
		return nil
	}
	require.NoError(t, err)
	packages := make([]string, 0, len(doc.Artifacts))
	for _, artifact := range doc.Artifacts {
		packages = append(packages, artifact.Package)
	}
	return packages
}

// Regression for the non-atomic multipart /upload: every file used to be written to
// storage before the duplicate check, so a rejected request overwrote stored files and
// left a partially recorded version (e.g. an electron-builder macOS build with two
// .blockmap files lost its latest-mac.yml). Conflicts must now be caught before any write.
func TestUploadRejectsBeforeWriting(t *testing.T) {
	const app = "uploadatomicapp"
	cleanupUpdaterApp(t, app)
	defer cleanupUpdaterApp(t, app)
	router := updaterFlowRouter()
	createUpdaterApp(t, router, app)
	defaultLayoutURL := func(version, ext string) string {
		return fmt.Sprintf("%s/%s-admin/nightly/macos/universalArch/%s-%s%s", s3Endpoint, app, app, version, ext)
	}

	var electronVersionID string
	t.Run("electron_builder_macos_with_both_blockmaps", func(t *testing.T) {
		electronVersionID = uploadLatestFlowVersion(t, router, app, "1.0.1", "macos", "electron-builder", "",
			"latest-mac.yml", app+"-1.0.1-mac.zip", app+"-1.0.1-mac.zip.blockmap", app+"-1.0.1.dmg", app+"-1.0.1.dmg.blockmap")
		require.ElementsMatch(t, []string{".yml", ".zip", ".blockmap", ".dmg", ".blockmap"}, storedVersionPackages(t, app, "1.0.1"))

		w := serveVersionCheck(t, fmt.Sprintf("/checkVersion?app_name=%s&version=0.0.0.1&channel=nightly&platform=macos&arch=universalArch&owner=admin&updater=electron-builder", app), nil)
		require.Equal(t, http.StatusFound, w.Code, w.Body.String())
		require.True(t, strings.HasSuffix(w.Header().Get("Location"), "/latest-mac.yml"), "redirected to %q", w.Header().Get("Location"))
	})

	t.Run("colliding_files_in_one_request_write_nothing", func(t *testing.T) {
		w := doUpdaterRequest(t, router, "/upload",
			fmt.Sprintf(`{"app_name":"%s","version":"1.0.2","channel":"nightly","publish":true,"platform":"macos","arch":"universalArch","updater":"manual"}`, app),
			[]updaterFeedFile{{name: "first.zip", content: []byte("first")}, {name: "second.dmg", content: []byte("dmg")}, {name: "third.zip", content: []byte("third")}})
		require.Equal(t, http.StatusBadRequest, w.Code, w.Body.String())
		var resp map[string]string
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
		require.Contains(t, resp["error"], "first.zip")
		require.Contains(t, resp["error"], "third.zip")

		require.Nil(t, storedVersionPackages(t, app, "1.0.2"), "rejected request must not record a version")
		require.NotEqual(t, http.StatusOK, storageObjectStatus(t, defaultLayoutURL("1.0.2", ".zip")), "rejected request must not write to storage")
		require.NotEqual(t, http.StatusOK, storageObjectStatus(t, defaultLayoutURL("1.0.2", ".dmg")), "rejected request must not write to storage")
	})

	t.Run("stored_conflict_keeps_existing_file_and_version", func(t *testing.T) {
		uploadLatestFlowVersion(t, router, app, "1.0.3", "macos", "manual", "", app+"-1.0.3.dmg")
		original := checkFileContent(t, defaultLayoutURL("1.0.3", ".dmg"))
		require.Equal(t, app+"-1.0.3.dmg", original)

		w := doUpdaterRequest(t, router, "/upload",
			fmt.Sprintf(`{"app_name":"%s","version":"1.0.3","channel":"nightly","publish":true,"platform":"macos","arch":"universalArch","updater":"manual"}`, app),
			[]updaterFeedFile{{name: "retry.zip", content: []byte("new zip")}, {name: "retry.dmg", content: []byte("replaced")}})
		require.Equal(t, http.StatusConflict, w.Code, w.Body.String())
		require.JSONEq(t, `{"error":"app with this name, version, platform, architecture and extension already exists"}`, w.Body.String())

		require.Equal(t, original, checkFileContent(t, defaultLayoutURL("1.0.3", ".dmg")), "stored file must not be overwritten")
		require.Equal(t, []string{".dmg"}, storedVersionPackages(t, app, "1.0.3"), "no artifact of the rejected request may be recorded")
		require.NotEqual(t, http.StatusOK, storageObjectStatus(t, defaultLayoutURL("1.0.3", ".zip")))
	})

	// /apps/update used to overwrite the stored object and then skip the artifact as a
	// duplicate, leaving hashes (and TUF targets) describing the old bytes.
	t.Run("apps_update_rejects_stored_file", func(t *testing.T) {
		require.NotEmpty(t, electronVersionID)
		dmgURL := fmt.Sprintf("%s/electron-builder/%s-admin/1.0.1/nightly/macos/universalArch/%s-1.0.1.dmg", s3Endpoint, app, app)
		original := checkFileContent(t, dmgURL)
		update := func(extra string, files ...updaterFeedFile) *httptest.ResponseRecorder {
			return doUpdaterRequest(t, router, "/apps/update",
				fmt.Sprintf(`{"id":"%s","app_name":"%s","version":"1.0.1","publish":true,"platform":"macos","arch":"universalArch","updater":"electron-builder"%s}`, electronVersionID, app, extra),
				files)
		}

		w := update(`,"channel":"nightly"`, updaterFeedFile{name: "latest-mac.yml", content: []byte("version: 1.0.1")}, updaterFeedFile{name: app + "-1.0.1.dmg", content: []byte("replaced")})
		require.Equal(t, http.StatusConflict, w.Code, w.Body.String())
		require.JSONEq(t, `{"error":"app with this name, version, platform, architecture and extension already exists"}`, w.Body.String())
		require.Equal(t, original, checkFileContent(t, dmgURL), "stored file must not be overwritten")

		w = update(`,"channel":"stable"`, updaterFeedFile{name: "latest-mac.yml", content: []byte("version: 1.0.1")}, updaterFeedFile{name: app + "-1.0.1.pkg", content: []byte("pkg")})
		require.Equal(t, http.StatusConflict, w.Code, w.Body.String())
		require.Contains(t, w.Body.String(), "cannot be changed")

		// An omitted channel keeps the version's own, and the feed may be re-sent with a new file.
		w = update("", updaterFeedFile{name: "latest-mac.yml", content: []byte("version: 1.0.1")}, updaterFeedFile{name: app + "-1.0.1.pkg", content: []byte("pkg")})
		require.Equal(t, http.StatusOK, w.Code, w.Body.String())
		require.Contains(t, storedVersionPackages(t, app, "1.0.1"), ".pkg")
		require.Equal(t, original, checkFileContent(t, dmgURL))
	})

	// .nupkg, .delta and .blockmap may repeat within a platform/arch, but the default
	// layout stores every one of them as {app}-{version}{ext}: a second file used to
	// overwrite the first and add a second artifact with the same link.
	t.Run("repeatable_extension_same_key_rejected", func(t *testing.T) {
		upload := func(name, content string) *httptest.ResponseRecorder {
			return doUpdaterRequest(t, router, "/upload",
				fmt.Sprintf(`{"app_name":"%s","version":"1.0.4","channel":"nightly","publish":true,"platform":"macos","arch":"universalArch","updater":"manual"}`, app),
				[]updaterFeedFile{{name: name, content: []byte(content)}})
		}
		w := upload("first.blockmap", "first")
		require.Equal(t, http.StatusOK, w.Code, w.Body.String())

		w = upload("second.blockmap", "second")
		require.Equal(t, http.StatusConflict, w.Code, w.Body.String())
		require.Equal(t, "first", checkFileContent(t, defaultLayoutURL("1.0.4", ".blockmap")), "stored file must not be overwritten")
		require.Equal(t, []string{".blockmap"}, storedVersionPackages(t, app, "1.0.4"))
	})

	t.Run("apps_update_rejects_same_repeatable_file", func(t *testing.T) {
		name := app + "-1.0.1-mac.zip.blockmap"
		w := doUpdaterRequest(t, router, "/apps/update",
			fmt.Sprintf(`{"id":"%s","app_name":"%s","version":"1.0.1","channel":"nightly","publish":true,"platform":"macos","arch":"universalArch","updater":"electron-builder"}`, electronVersionID, app),
			[]updaterFeedFile{{name: "latest-mac.yml", content: []byte("version: 1.0.1")}, {name: name, content: []byte("replaced")}})
		require.Equal(t, http.StatusConflict, w.Code, w.Body.String())
		blockmapURL := fmt.Sprintf("%s/electron-builder/%s-admin/1.0.1/nightly/macos/universalArch/%s", s3Endpoint, app, name)
		require.Equal(t, name, checkFileContent(t, blockmapURL), "stored file must not be overwritten")
	})

	// The channel is part of the storage key; an update that omits it must still place
	// files in the version's channel folder, so a re-sent file meets its stored key.
	t.Run("apps_update_without_channel_uses_version_channel", func(t *testing.T) {
		name := app + "-1.0.1-mac.zip.blockmap"
		w := doUpdaterRequest(t, router, "/apps/update",
			fmt.Sprintf(`{"id":"%s","app_name":"%s","version":"1.0.1","publish":true,"platform":"macos","arch":"universalArch","updater":"electron-builder"}`, electronVersionID, app),
			[]updaterFeedFile{{name: "latest-mac.yml", content: []byte("version: 1.0.1")}, {name: name, content: []byte("replaced")}})
		require.Equal(t, http.StatusConflict, w.Code, w.Body.String())

		var doc model.SpecificApp
		id, err := primitive.ObjectIDFromHex(electronVersionID)
		require.NoError(t, err)
		require.NoError(t, mongoDatabase.Collection("apps").FindOne(context.Background(), bson.M{"_id": id}).Decode(&doc))
		for _, artifact := range doc.Artifacts {
			require.Contains(t, artifact.Link, "/1.0.1/nightly/macos/", "artifact stored outside the version's channel folder")
		}
	})

	// A concurrent request passes the pre-write check before either is recorded; the
	// append itself must still refuse a second artifact with the same link.
	t.Run("repository_refuses_same_link_twice", func(t *testing.T) {
		ctxQuery := map[string]interface{}{
			"app_name": app, "version": "1.0.4", "channel": "nightly", "platform": "macos", "arch": "universalArch",
			"signature": "", "changelog": "", "publish": "true",
		}
		link := defaultLayoutURL("1.0.4", ".blockmap")
		ctx := utils.WithPrincipal(context.Background(), utils.Principal{Type: utils.PrincipalAdmin, Username: "admin"})
		_, err := appDB.Upload(ctxQuery, link, ".blockmap", "admin", ctx, redisClient, viper.GetViper(), false)
		require.Error(t, err)
		require.Contains(t, err.Error(), "already exists")

		_, err = appDB.Upload(ctxQuery, defaultLayoutURL("1.0.4", "-other.blockmap"), ".blockmap", "admin", ctx, redisClient, viper.GetViper(), false)
		require.NoError(t, err, "a different link of a repeatable extension may still be added")
		require.Equal(t, []string{".blockmap", ".blockmap"}, storedVersionPackages(t, app, "1.0.4"))
	})
}

// Channel, platform and arch names are unique only per owner. The "required" checks
// stay instance-wide by design, but a name lookup must only see the caller's own
// entities: another admin's channel used to pass validation (and the upload then wrote
// to storage before failing), and /checkVersion resolved the updaters of whichever
// admin's platform with that name came first.
func TestEntityLookupsAreOwnerScoped(t *testing.T) {
	const app = "ownerscopeapp"
	const platform = "ownerscopedplatform"
	ctx := context.Background()
	cleanupMeta := func() {
		_, err := mongoDatabase.Collection("apps_meta").DeleteMany(ctx, bson.M{"$or": bson.A{
			bson.M{"channel_name": "secondonlychannel"},
			bson.M{"platform_name": bson.M{"$in": bson.A{"secondonlyplatform", platform}}},
			bson.M{"arch_id": "secondonlyarch"},
		}})
		require.NoError(t, err)
	}
	cleanupUpdaterApp(t, app)
	cleanupMeta()
	defer cleanupMeta()
	defer cleanupUpdaterApp(t, app)

	h := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	metaRouter := gin.Default()
	metaRouter.Use(utils.AuthMiddleware(mongoDatabase))
	metaRouter.POST("/channel/create", func(c *gin.Context) { h.CreateChannel(c) })
	metaRouter.POST("/platform/create", func(c *gin.Context) { h.CreatePlatform(c) })
	metaRouter.POST("/arch/create", func(c *gin.Context) { h.CreateArch(c) })
	createMeta := func(token, path, body string) {
		t.Helper()
		req, err := http.NewRequest("POST", path, strings.NewReader(body))
		require.NoError(t, err)
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+token)
		w := httptest.NewRecorder()
		metaRouter.ServeHTTP(w, req)
		require.Equal(t, http.StatusOK, w.Code, "%s %s: %s", path, body, w.Body.String())
	}

	// The second admin's platform is created first, so an owner-less lookup finds it first.
	createMeta(authTokenSecondUser, "/channel/create", `{"channel":"secondonlychannel"}`)
	createMeta(authTokenSecondUser, "/platform/create", `{"platform":"secondonlyplatform"}`)
	createMeta(authTokenSecondUser, "/arch/create", `{"arch":"secondonlyarch"}`)
	createMeta(authTokenSecondUser, "/platform/create", `{"platform":"`+platform+`","updaters":[{"type":"manual","default":true}]}`)
	createMeta(authToken, "/platform/create", `{"platform":"`+platform+`","updaters":[{"type":"manual","default":false},{"type":"tauri","default":true}]}`)

	router := updaterFlowRouter()
	createUpdaterApp(t, router, app)

	t.Run("another_admins_entities_are_unknown", func(t *testing.T) {
		cases := []struct{ channel, platform, arch, wantError string }{
			{"secondonlychannel", "macos", "universalArch", "Channel does not exist"},
			{"nightly", "secondonlyplatform", "universalArch", "Platform does not exist"},
			{"nightly", "macos", "secondonlyarch", "Arch does not exist"},
		}
		for _, tc := range cases {
			w := doUpdaterRequest(t, router, "/upload",
				fmt.Sprintf(`{"app_name":"%s","version":"1.0.0","channel":"%s","publish":true,"platform":"%s","arch":"%s","updater":"manual"}`, app, tc.channel, tc.platform, tc.arch),
				[]updaterFeedFile{{name: "build.dmg", content: []byte("dmg")}})
			require.Equal(t, http.StatusBadRequest, w.Code, w.Body.String())
			require.Contains(t, w.Body.String(), tc.wantError)
			url := fmt.Sprintf("%s/%s-admin/%s/%s/%s/%s-1.0.0.dmg", s3Endpoint, app, tc.channel, tc.platform, tc.arch, app)
			require.NotEqual(t, http.StatusOK, storageObjectStatus(t, url), "rejected upload must not write to storage")
		}
		require.Nil(t, storedVersionPackages(t, app, "1.0.0"))
	})

	t.Run("checkversion_uses_own_platform_updaters", func(t *testing.T) {
		uploadLatestFlowVersion(t, router, app, "1.0.1", platform, "tauri", `,"signature":"c2ln"`, app+".app.tar.gz")
		w := serveVersionCheck(t, fmt.Sprintf("/checkVersion?app_name=%s&version=0.0.1&channel=nightly&platform=%s&arch=universalArch&owner=admin", app, platform), nil)
		require.Equal(t, http.StatusOK, w.Code, w.Body.String())
		var resp map[string]interface{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
		require.Equal(t, "1.0.1", resp["version"], "default updater must be the caller's tauri, got %s", w.Body.String())
		require.NotContains(t, resp, "update_available")
	})
}

// ============================================================================
// Presigned upload flow (self-contained block).
// init -> direct PUT to storage -> complete, against the real configured storage.
// Pins the contract TUF publish relies on: client-declared hashes are stored
// with hashes_verified=false, feed files hashed by faynoSync with true.
// Reuses channels (nightly), platforms (windows/universalPlatform) and arch
// (universalArch) created earlier in the ordered suite.
// ============================================================================

type presignedInitResponse struct {
	UploadID string `json:"upload_id"`
	Files    []struct {
		Name    string            `json:"name"`
		Method  string            `json:"method"`
		URL     string            `json:"url"`
		Headers map[string]string `json:"headers"`
	} `json:"files"`
}

func presignedManifest(files []updaterFeedFile) string {
	entries := make([]map[string]interface{}, 0, len(files))
	for _, f := range files {
		md := md5.Sum(f.content)
		s256 := sha256.Sum256(f.content)
		s512 := sha512.Sum512(f.content)
		entries = append(entries, map[string]interface{}{
			"name":   f.name,
			"md5":    hex.EncodeToString(md[:]),
			"sha256": hex.EncodeToString(s256[:]),
			"sha512": hex.EncodeToString(s512[:]),
			"length": len(f.content),
		})
	}
	raw, _ := json.Marshal(entries)
	return string(raw)
}

func doPresignedInit(t *testing.T, router *gin.Engine, token, payload, manifest string, inline []updaterFeedFile) *httptest.ResponseRecorder {
	t.Helper()
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	for _, f := range inline {
		part, err := writer.CreateFormFile("file", f.name)
		require.NoError(t, err)
		_, err = part.Write(f.content)
		require.NoError(t, err)
	}
	require.NoError(t, writer.WriteField("data", payload))
	if manifest != "" {
		require.NoError(t, writer.WriteField("files", manifest))
	}
	require.NoError(t, writer.Close())
	req, err := http.NewRequest("POST", "/upload/init", body)
	require.NoError(t, err)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	req.Header.Set("Authorization", "Bearer "+token)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	return w
}

func doPresignedComplete(t *testing.T, router *gin.Engine, token, uploadID string) *httptest.ResponseRecorder {
	t.Helper()
	req, err := http.NewRequest("POST", "/upload/complete", strings.NewReader(fmt.Sprintf(`{"upload_id":"%s"}`, uploadID)))
	require.NoError(t, err)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+token)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	return w
}

// presignedMD5OnlyManifest declares only what init requires: sha256/sha512 are needed
// solely for TUF, and the length is taken from storage.
func presignedMD5OnlyManifest(files []updaterFeedFile) string {
	entries := make([]map[string]interface{}, 0, len(files))
	for _, f := range files {
		md := md5.Sum(f.content)
		entries = append(entries, map[string]interface{}{"name": f.name, "md5": hex.EncodeToString(md[:])})
	}
	raw, _ := json.Marshal(entries)
	return string(raw)
}

func presignedInitOK(t *testing.T, router *gin.Engine, payload string, presigned, inline []updaterFeedFile) presignedInitResponse {
	t.Helper()
	return presignedInitWithManifest(t, router, payload, presignedManifest(presigned), len(presigned), inline)
}

func presignedInitWithManifest(t *testing.T, router *gin.Engine, payload, manifest string, files int, inline []updaterFeedFile) presignedInitResponse {
	t.Helper()
	w := doPresignedInit(t, router, authToken, payload, manifest, inline)
	require.Equal(t, http.StatusOK, w.Code, "init: %s", w.Body.String())
	var resp presignedInitResponse
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	require.Len(t, resp.UploadID, 32)
	require.Len(t, resp.Files, files)
	return resp
}

// putPresigned sends the bytes straight to storage, the way the CLI or curl would.
func putPresigned(t *testing.T, upload presignedInitResponse, name string, content []byte) (int, string) {
	t.Helper()
	for _, f := range upload.Files {
		if f.Name != name {
			continue
		}
		require.Equal(t, http.MethodPut, f.Method)
		req, err := http.NewRequest(http.MethodPut, f.URL, bytes.NewReader(content))
		require.NoError(t, err)
		for k, v := range f.Headers {
			req.Header.Set(k, v)
		}
		req.ContentLength = int64(len(content))
		resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()
		respBody, _ := io.ReadAll(resp.Body)
		return resp.StatusCode, string(respBody)
	}
	t.Fatalf("file %s is not in the init response", name)
	return 0, ""
}

func pendingUploadKeys(t *testing.T, uploadID string) []string {
	t.Helper()
	var doc struct {
		Files []struct {
			PendingKey string `bson:"pending_key"`
		} `bson:"files"`
	}
	require.NoError(t, mongoDatabase.Collection("pending_uploads").FindOne(context.Background(), bson.M{"_id": uploadID}).Decode(&doc))
	keys := make([]string, 0, len(doc.Files))
	for _, f := range doc.Files {
		keys = append(keys, f.PendingKey)
	}
	return keys
}

func uploadedVersion(t *testing.T, w *httptest.ResponseRecorder) model.SpecificApp {
	t.Helper()
	require.Equal(t, http.StatusOK, w.Code, "complete: %s", w.Body.String())
	var resp map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
	id, _ := resp["uploadResult.Uploaded"].(string)
	objectID, err := primitive.ObjectIDFromHex(id)
	require.NoError(t, err, "no upload id in %s", w.Body.String())
	var doc model.SpecificApp
	require.NoError(t, mongoDatabase.Collection("apps").FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&doc))
	return doc
}

func artifactByLinkSuffix(t *testing.T, doc model.SpecificApp, suffix string) model.Artifact {
	t.Helper()
	for _, a := range doc.Artifacts {
		if strings.HasSuffix(a.Link, suffix) {
			return a
		}
	}
	t.Fatalf("no artifact with link suffix %s in %+v", suffix, doc.Artifacts)
	return model.Artifact{}
}

func TestPresignedUploadFlow(t *testing.T) {
	storageClient, err := utils.NewStorageFactory(viper.GetViper()).CreateStorageClient()
	require.NoError(t, err)
	uploader, ok := storageClient.(storage.PresignedUploader)
	if !ok {
		t.Skipf("storage driver %q does not support presigned uploads", viper.GetString("STORAGE_DRIVER"))
	}

	cleanupUpdaterApp(t, "presignedapp")
	cleanupUpdaterApp(t, "presignedprivate")
	defer cleanupUpdaterApp(t, "presignedapp")
	defer cleanupUpdaterApp(t, "presignedprivate")
	router := updaterFlowRouter()
	createUpdaterApp(t, router, "presignedapp")

	nupkgName := "presignedapp-2.0.0-full.nupkg"
	nupkg := updaterFeedFile{name: nupkgName, content: []byte(strings.Repeat("presigned nupkg payload ", 64))}
	s256 := sha256.Sum256(nupkg.content)
	s1 := fmt.Sprintf("%x", s256[:20])
	feed := updaterFeedFile{
		name:    "releases.nightly.json",
		content: []byte(fmt.Sprintf(`{"Assets":[{"Type":"Full","FileName":"%s","SHA1":"%s","SHA256":"%x","Size":%d}]}`, nupkgName, s1, s256, len(nupkg.content))),
	}
	payload := `{"app_name":"presignedapp","version":"2.0.0","channel":"nightly","publish":true,"critical":false,"platform":"windows","arch":"universalArch","updater":"velopack","changelog":"presigned release"}`

	t.Run("init_rejects_before_any_transfer", func(t *testing.T) {
		cases := []struct {
			name     string
			manifest string
			inline   []updaterFeedFile
			want     int
		}{
			{"missing_manifest", "", []updaterFeedFile{feed}, http.StatusBadRequest},
			{"feed_must_be_inline", presignedManifest([]updaterFeedFile{nupkg, feed}), nil, http.StatusBadRequest},
			{"package_must_not_be_inline", presignedManifest([]updaterFeedFile{nupkg}), []updaterFeedFile{feed, {name: "extra.exe", content: []byte("x")}}, http.StatusBadRequest},
			{"updater_rules_apply", presignedManifest([]updaterFeedFile{{name: "presignedapp-2.0.0.exe", content: []byte("x")}}), []updaterFeedFile{feed}, http.StatusBadRequest},
			{"path_in_name", presignedManifest([]updaterFeedFile{{name: "../" + nupkgName, content: nupkg.content}}), []updaterFeedFile{feed}, http.StatusBadRequest},
		}
		for _, tc := range cases {
			w := doPresignedInit(t, router, authToken, payload, tc.manifest, tc.inline)
			assert.Equal(t, tc.want, w.Code, "%s: %s", tc.name, w.Body.String())
		}
	})

	upload := presignedInitOK(t, router, payload, []updaterFeedFile{nupkg}, []updaterFeedFile{feed})
	pendingKeys := pendingUploadKeys(t, upload.UploadID)
	require.Len(t, pendingKeys, 2)

	t.Run("complete_before_put_is_rejected", func(t *testing.T) {
		w := doPresignedComplete(t, router, authToken, upload.UploadID)
		require.Equal(t, http.StatusUnprocessableEntity, w.Code, w.Body.String())
		assert.Contains(t, w.Body.String(), "not uploaded")
	})

	t.Run("storage_rejects_tampered_bytes", func(t *testing.T) {
		tampered := append([]byte{}, nupkg.content...)
		tampered[0] ^= 0xff
		status, body := putPresigned(t, upload, nupkgName, tampered)
		require.GreaterOrEqual(t, status, 400, "tampered payload accepted: %s", body)
	})

	t.Run("other_user_cannot_complete", func(t *testing.T) {
		w := doPresignedComplete(t, router, authTokenSecondUser, upload.UploadID)
		require.Equal(t, http.StatusNotFound, w.Code, w.Body.String())
	})

	status, body := putPresigned(t, upload, nupkgName, nupkg.content)
	require.Less(t, status, 300, "PUT: %s", body)

	doc := uploadedVersion(t, doPresignedComplete(t, router, authToken, upload.UploadID))

	t.Run("artifacts_carry_hash_provenance", func(t *testing.T) {
		pkg := artifactByLinkSuffix(t, doc, "/"+nupkgName)
		s512 := sha512.Sum512(nupkg.content)
		assert.False(t, pkg.HashesVerified, "client-declared hashes must wait for TUF publish verification")
		assert.Equal(t, hex.EncodeToString(s256[:]), pkg.Hashes["sha256"])
		assert.Equal(t, hex.EncodeToString(s512[:]), pkg.Hashes["sha512"])
		assert.Equal(t, int64(len(nupkg.content)), pkg.Length)
		assert.False(t, pkg.IsFeed)

		feedArtifact := artifactByLinkSuffix(t, doc, "/releases.nightly.json")
		assert.True(t, feedArtifact.IsFeed)
		assert.True(t, feedArtifact.HashesVerified, "inline feed is hashed by faynoSync itself")
	})

	t.Run("objects_moved_out_of_staging", func(t *testing.T) {
		ctx := context.Background()
		bucket := viper.GetString("S3_BUCKET_NAME")
		key := fmt.Sprintf("velopack/admin/presignedapp/windows/universalArch/%s", nupkgName)
		stat, err := uploader.StatObject(ctx, bucket, key)
		require.NoError(t, err)
		md := md5.Sum(nupkg.content)
		assert.Equal(t, int64(len(nupkg.content)), stat.Size)
		assert.Equal(t, hex.EncodeToString(md[:]), stat.MD5)
		for _, pendingKey := range pendingKeys {
			_, err := uploader.StatObject(ctx, bucket, pendingKey)
			assert.ErrorIs(t, err, storage.ErrObjectNotFound, "staged object %s must be deleted", pendingKey)
		}
		count, err := mongoDatabase.Collection("pending_uploads").CountDocuments(ctx, bson.M{"_id": upload.UploadID})
		require.NoError(t, err)
		assert.Zero(t, count)
	})

	t.Run("feed_materialized", func(t *testing.T) {
		assets := parseVelopackFeed(t, checkFileContent(t, fmt.Sprintf("%s/velopack/admin/presignedapp/windows/universalArch/releases.nightly.json", s3Endpoint)))
		require.Contains(t, assets, "2.0.0", "published presigned version must be in the feed")
	})

	t.Run("complete_is_single_use", func(t *testing.T) {
		w := doPresignedComplete(t, router, authToken, upload.UploadID)
		require.Equal(t, http.StatusNotFound, w.Code, w.Body.String())
	})

	t.Run("init_detects_existing_artifact", func(t *testing.T) {
		w := doPresignedInit(t, router, authToken, payload, presignedManifest([]updaterFeedFile{nupkg}), []updaterFeedFile{feed})
		require.Equal(t, http.StatusConflict, w.Code, w.Body.String())
	})

	velopackRelease := func(version, suffix string) (updaterFeedFile, updaterFeedFile) {
		pkg := updaterFeedFile{
			name:    fmt.Sprintf("presignedapp-%s-full.nupkg", version),
			content: []byte(strings.Repeat("presigned "+version+" "+suffix+" payload ", 32)),
		}
		sum := sha256.Sum256(pkg.content)
		releases := updaterFeedFile{
			name:    "releases.nightly.json",
			content: []byte(fmt.Sprintf(`{"Assets":[{"Type":"Full","FileName":"%s","SHA1":"%x","SHA256":"%x","Size":%d}]}`, pkg.name, sum[:20], sum, len(pkg.content))),
		}
		return pkg, releases
	}
	velopackPayload := func(version, channel, platform string) string {
		return fmt.Sprintf(`{"app_name":"presignedapp","version":"%s","channel":"%s","publish":true,"critical":false,"platform":"%s","arch":"universalArch","updater":"velopack"}`, version, channel, platform)
	}
	platformID := func(name string) primitive.ObjectID {
		var meta struct {
			ID primitive.ObjectID `bson:"_id"`
		}
		require.NoError(t, mongoDatabase.Collection("apps_meta").FindOne(context.Background(), bson.M{"platform_name": name, "owner": "admin"}).Decode(&meta))
		return meta.ID
	}

	t.Run("another_platform_appends_to_the_same_version", func(t *testing.T) {
		pkg, releases := velopackRelease("2.0.0", "macos")
		macUpload := presignedInitOK(t, router, velopackPayload("2.0.0", "nightly", "macos"), []updaterFeedFile{pkg}, []updaterFeedFile{releases})
		status, body := putPresigned(t, macUpload, pkg.name, pkg.content)
		require.Less(t, status, 300, "PUT: %s", body)
		appended := uploadedVersion(t, doPresignedComplete(t, router, authToken, macUpload.UploadID))
		assert.Equal(t, doc.ID, appended.ID, "a second platform must land in the existing version")
		assert.Equal(t, platformID("macos"), artifactByLinkSuffix(t, appended, "/macos/universalArch/"+pkg.name).Platform)
		assert.Equal(t, platformID("windows"), artifactByLinkSuffix(t, appended, "/windows/universalArch/"+nupkgName).Platform)
	})

	t.Run("channel_of_a_version_is_immutable", func(t *testing.T) {
		pkg, releases := velopackRelease("2.0.0", "stable")
		releases.name = "releases.stable.json"
		w := doPresignedInit(t, router, authToken, velopackPayload("2.0.0", "stable", "macosSquirrel"), presignedManifest([]updaterFeedFile{pkg}), []updaterFeedFile{releases})
		require.Equal(t, http.StatusConflict, w.Code, w.Body.String())
		assert.Contains(t, w.Body.String(), "cannot be changed")

		w = doUpdaterRequest(t, router, "/upload", velopackPayload("2.0.0", "stable", "macosSquirrel"), []updaterFeedFile{releases, pkg})
		require.NotEqual(t, http.StatusOK, w.Code, "multipart upload must not append to a version of another channel: %s", w.Body.String())
		assert.Contains(t, w.Body.String(), "cannot be changed")
	})

	// Two platforms of a new version completed at the same time, the way parallel CI jobs
	// do: both must end up in one version document, each artifact with its own platform.
	t.Run("parallel_completes_build_one_version", func(t *testing.T) {
		platforms := []string{"windows", "macos"}
		uploads := make([]presignedInitResponse, len(platforms))
		pkgs := make([]updaterFeedFile, len(platforms))
		for i, platform := range platforms {
			pkg, releases := velopackRelease("2.1.0", platform)
			pkgs[i] = pkg
			uploads[i] = presignedInitOK(t, router, velopackPayload("2.1.0", "nightly", platform), []updaterFeedFile{pkg}, []updaterFeedFile{releases})
			status, body := putPresigned(t, uploads[i], pkg.name, pkg.content)
			require.Less(t, status, 300, "PUT %s: %s", platform, body)
		}

		codes := make([]int, len(platforms))
		bodies := make([]string, len(platforms))
		done := make(chan struct{})
		for i := range platforms {
			go func(i int) {
				defer func() { done <- struct{}{} }()
				req, _ := http.NewRequest("POST", "/upload/complete", strings.NewReader(fmt.Sprintf(`{"upload_id":"%s"}`, uploads[i].UploadID)))
				req.Header.Set("Content-Type", "application/json")
				req.Header.Set("Authorization", "Bearer "+authToken)
				w := httptest.NewRecorder()
				router.ServeHTTP(w, req)
				codes[i], bodies[i] = w.Code, w.Body.String()
			}(i)
		}
		for range platforms {
			<-done
		}
		for i, platform := range platforms {
			require.Equal(t, http.StatusOK, codes[i], "complete %s: %s", platform, bodies[i])
		}

		count, err := mongoDatabase.Collection("apps").CountDocuments(context.Background(), bson.M{"app_id": doc.AppID, "version": "2.1.0"})
		require.NoError(t, err)
		require.EqualValues(t, 1, count, "concurrent uploads must not create the version twice")

		var version model.SpecificApp
		require.NoError(t, mongoDatabase.Collection("apps").FindOne(context.Background(), bson.M{"app_id": doc.AppID, "version": "2.1.0"}).Decode(&version))
		require.Len(t, version.Artifacts, 2*len(platforms), "every platform contributes a package and a feed; none may be lost")
		for i, platform := range platforms {
			pkg := artifactByLinkSuffix(t, version, "/"+platform+"/universalArch/"+pkgs[i].name)
			assert.Equal(t, platformID(platform), pkg.Platform, "artifact stored under %s must carry its own platform", platform)
		}
	})

	// Final object keys are deterministic, so an artifact recorded after init must stop
	// complete before its object is overwritten with different bytes.
	t.Run("complete_rechecks_conflict", func(t *testing.T) {
		first, firstReleases := velopackRelease("2.2.0", "first")
		second, secondReleases := velopackRelease("2.2.0", "second")
		require.Equal(t, first.name, second.name)
		firstUpload := presignedInitOK(t, router, velopackPayload("2.2.0", "nightly", "windows"), []updaterFeedFile{first}, []updaterFeedFile{firstReleases})
		secondUpload := presignedInitOK(t, router, velopackPayload("2.2.0", "nightly", "windows"), []updaterFeedFile{second}, []updaterFeedFile{secondReleases})
		status, body := putPresigned(t, firstUpload, first.name, first.content)
		require.Less(t, status, 300, "PUT first: %s", body)
		status, body = putPresigned(t, secondUpload, second.name, second.content)
		require.Less(t, status, 300, "PUT second: %s", body)

		uploadedVersion(t, doPresignedComplete(t, router, authToken, firstUpload.UploadID))
		w := doPresignedComplete(t, router, authToken, secondUpload.UploadID)
		require.Equal(t, http.StatusConflict, w.Code, w.Body.String())

		stat, err := uploader.StatObject(context.Background(), viper.GetString("S3_BUCKET_NAME"), "velopack/admin/presignedapp/windows/universalArch/"+first.name)
		require.NoError(t, err)
		md := md5.Sum(first.content)
		assert.Equal(t, hex.EncodeToString(md[:]), stat.MD5, "the recorded artifact must keep its bytes")
	})

	// A complete that fails before any artifact is recorded must stay retryable.
	t.Run("complete_releases_when_nothing_recorded", func(t *testing.T) {
		pkg, releases := velopackRelease("2.3.0", "retry")
		retryUpload := presignedInitOK(t, router, velopackPayload("2.3.0", "nightly", "windows"), []updaterFeedFile{pkg}, []updaterFeedFile{releases})
		status, body := putPresigned(t, retryUpload, pkg.name, pkg.content)
		require.Less(t, status, 300, "PUT: %s", body)

		ctx := context.Background()
		metaCollection := mongoDatabase.Collection("apps_meta")
		var arch bson.M
		require.NoError(t, metaCollection.FindOne(ctx, bson.M{"arch_id": "universalArch", "owner": "admin"}).Decode(&arch))
		_, err := metaCollection.DeleteOne(ctx, bson.M{"_id": arch["_id"]})
		require.NoError(t, err)
		restored := false
		restore := func() {
			if !restored {
				_, err := metaCollection.InsertOne(ctx, arch)
				require.NoError(t, err)
				restored = true
			}
		}
		defer restore()

		for attempt := 0; attempt < 2; attempt++ {
			w := doPresignedComplete(t, router, authToken, retryUpload.UploadID)
			require.Equal(t, http.StatusInternalServerError, w.Code, "attempt %d: %s", attempt, w.Body.String())
			assert.Contains(t, w.Body.String(), "arch not found")
		}
		var pending struct {
			State string `bson:"state"`
		}
		require.NoError(t, mongoDatabase.Collection("pending_uploads").FindOne(ctx, bson.M{"_id": retryUpload.UploadID}).Decode(&pending))
		assert.Equal(t, "pending", pending.State)

		restore()
		uploadedVersion(t, doPresignedComplete(t, router, authToken, retryUpload.UploadID))
	})

	t.Run("private_app_md5_only_manifest", func(t *testing.T) {
		w := doUpdaterRequest(t, router, "/app/create", `{"app": "presignedprivate", "private": "true"}`, nil)
		require.Equal(t, http.StatusOK, w.Code, w.Body.String())

		file := updaterFeedFile{name: "presignedprivate.zip", content: []byte("private presigned payload")}
		privateInit := presignedInitWithManifest(t, router,
			`{"app_name":"presignedprivate","version":"0.0.1","channel":"nightly","publish":true,"critical":false,"platform":"universalPlatform","arch":"universalArch"}`,
			presignedMD5OnlyManifest([]updaterFeedFile{file}), 1, nil)
		_, lengthSigned := privateInit.Files[0].Headers["Content-Length"]
		assert.False(t, lengthSigned, "an undeclared length must not be signed")
		status, body := putPresigned(t, privateInit, file.name, file.content)
		require.Less(t, status, 300, "PUT: %s", body)

		privateDoc := uploadedVersion(t, doPresignedComplete(t, router, authToken, privateInit.UploadID))
		require.Len(t, privateDoc.Artifacts, 1)
		link := privateDoc.Artifacts[0].Link
		require.True(t, strings.HasPrefix(link, apiUrl+"/download?key="), "private artifact must be served through the API: %s", link)
		assert.False(t, privateDoc.Artifacts[0].HashesVerified)
		assert.Empty(t, privateDoc.Artifacts[0].Hashes, "undeclared hashes stay empty; such an artifact is not signable by TUF")
		assert.Equal(t, int64(len(file.content)), privateDoc.Artifacts[0].Length, "length must come from storage when not declared")

		key, err := url.QueryUnescape(strings.TrimPrefix(link, apiUrl+"/download?key="))
		require.NoError(t, err)
		stat, err := uploader.StatObject(context.Background(), viper.GetString("S3_BUCKET_NAME_PRIVATE"), key)
		require.NoError(t, err, "object must be in the private bucket")
		assert.Equal(t, int64(len(file.content)), stat.Size)
	})
}

// TestPresignedUploadTeamUserScope checks that a team user's allowed lists are enforced by
// multipart and presigned uploads before anything reaches storage. Self-contained: it
// creates its own apps and team user and removes them afterwards.
func TestPresignedUploadTeamUserScope(t *testing.T) {
	storageClient, err := utils.NewStorageFactory(viper.GetViper()).CreateStorageClient()
	require.NoError(t, err)
	uploader, ok := storageClient.(storage.PresignedUploader)
	if !ok {
		t.Skipf("storage driver %q does not support presigned uploads", viper.GetString("STORAGE_DRIVER"))
	}

	ctx := context.Background()
	const (
		teamUser     = "teamscopeuser"
		allowedApp   = "teamscopeapp"
		forbiddenApp = "teamscopeother"
		apiTokenName = "teamscope-token"
	)
	teamUsers := mongoDatabase.Collection("team_users")
	pendingUploads := mongoDatabase.Collection("pending_uploads")
	bucket := viper.GetString("S3_BUCKET_NAME")
	var writtenKeys []string
	cleanup := func() {
		_, _ = teamUsers.DeleteOne(ctx, bson.M{"username": teamUser})
		_, _ = mongoDatabase.Collection("api_tokens").DeleteMany(ctx, bson.M{"name": apiTokenName, "owner": "admin"})
		_, _ = pendingUploads.DeleteMany(ctx, bson.M{"username": teamUser})
		if len(writtenKeys) > 0 {
			_ = storageClient.DeleteObjects(ctx, bucket, writtenKeys)
		}
		cleanupUpdaterApp(t, allowedApp)
		cleanupUpdaterApp(t, forbiddenApp)
	}
	cleanup()
	defer cleanup()

	adminRouter := updaterFlowRouter()
	createUpdaterApp(t, adminRouter, allowedApp)
	createUpdaterApp(t, adminRouter, forbiddenApp)

	metaID := func(field, value string) string {
		var meta struct {
			ID primitive.ObjectID `bson:"_id"`
		}
		require.NoError(t, mongoDatabase.Collection("apps_meta").FindOne(ctx, bson.M{field: value, "owner": "admin"}).Decode(&meta))
		return meta.ID.Hex()
	}
	appID := metaID("app_name", allowedApp)
	channelID := metaID("channel_name", "nightly")
	platformID := metaID("platform_name", "universalPlatform")
	archID := metaID("arch_id", "universalArch")

	h := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router := gin.Default()
	router.POST("/login", func(c *gin.Context) { h.Login(c) })
	router.POST("/user/create", utils.AuthMiddleware(mongoDatabase), utils.AdminOnlyMiddleware(mongoDatabase), func(c *gin.Context) { h.CreateTeamUser(c) })
	router.POST("/token/create", utils.AuthMiddleware(mongoDatabase), utils.AdminOnlyMiddleware(mongoDatabase), func(c *gin.Context) { h.CreateToken(c) })
	uploadPermission := utils.CheckPermission(utils.PermissionUpload, utils.ResourceApps, mongoDatabase)
	router.POST("/upload", utils.AuthMiddleware(mongoDatabase), uploadPermission, func(c *gin.Context) { h.UploadApp(c) })
	router.POST("/upload/init", utils.AuthMiddleware(mongoDatabase), uploadPermission, func(c *gin.Context) { h.InitPresignedUpload(c) })
	router.POST("/upload/complete", utils.AuthMiddleware(mongoDatabase), uploadPermission, func(c *gin.Context) { h.CompletePresignedUpload(c) })

	createBody, err := json.Marshal(map[string]interface{}{
		"username": teamUser,
		"password": "password123",
		"permissions": map[string]interface{}{
			"apps":      map[string]interface{}{"upload": true, "allowed": []string{appID}},
			"channels":  map[string]interface{}{"allowed": []string{channelID}},
			"platforms": map[string]interface{}{"allowed": []string{platformID}},
			"archs":     map[string]interface{}{"allowed": []string{archID}},
		},
	})
	require.NoError(t, err)
	req, err := http.NewRequest("POST", "/user/create", bytes.NewReader(createBody))
	require.NoError(t, err)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+authToken)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	require.Equal(t, http.StatusOK, w.Code, "create team user: %s", w.Body.String())

	var stored model.TeamUser
	require.NoError(t, teamUsers.FindOne(ctx, bson.M{"username": teamUser}).Decode(&stored))
	require.Equal(t, []string{appID}, stored.Permissions.Apps.Allowed)
	require.Equal(t, []string{channelID}, stored.Permissions.Channels.Allowed)

	req, err = http.NewRequest("POST", "/login", strings.NewReader(fmt.Sprintf(`{"username":"%s","password":"password123"}`, teamUser)))
	require.NoError(t, err)
	req.Header.Set("Content-Type", "application/json")
	w = httptest.NewRecorder()
	router.ServeHTTP(w, req)
	require.Equal(t, http.StatusOK, w.Code, "login: %s", w.Body.String())
	var login map[string]interface{}
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &login))
	token, _ := login["token"].(string)
	require.NotEmpty(t, token)

	revoke := func(t *testing.T, field, id string) func() {
		t.Helper()
		_, err := teamUsers.UpdateOne(ctx, bson.M{"username": teamUser}, bson.M{"$pull": bson.M{field: id}})
		require.NoError(t, err)
		return func() {
			_, err := teamUsers.UpdateOne(ctx, bson.M{"username": teamUser}, bson.M{"$addToSet": bson.M{field: id}})
			require.NoError(t, err)
		}
	}
	payload := func(app, version, channel string) string {
		return fmt.Sprintf(`{"app_name":"%s","version":"%s","channel":"%s","publish":true,"critical":false,"platform":"universalPlatform","arch":"universalArch"}`, app, version, channel)
	}
	artifact := func(app, version string) updaterFeedFile {
		return updaterFeedFile{name: app + ".zip", content: []byte("team scope payload " + app + " " + version)}
	}
	// objectKey is where an upload with these parameters lands, so the test can prove nothing was written there.
	objectKey := func(app, version, channel, name string) string {
		key := utils.BuildObjectPlacement(map[string]interface{}{
			"app_name": app, "version": version, "channel": channel, "platform": "universalPlatform", "arch": "universalArch",
		}, "admin", name, viper.GetViper(), false).Key
		writtenKeys = append(writtenKeys, key)
		return key
	}
	assertNotStored := func(t *testing.T, key string) {
		t.Helper()
		_, err := uploader.StatObject(ctx, bucket, key)
		assert.ErrorIs(t, err, storage.ErrObjectNotFound, "a rejected upload must not write %s", key)
	}
	multipartUploadAs := func(t *testing.T, bearer, data string, file updaterFeedFile) *httptest.ResponseRecorder {
		t.Helper()
		body := &bytes.Buffer{}
		writer := multipart.NewWriter(body)
		part, err := writer.CreateFormFile("file", file.name)
		require.NoError(t, err)
		_, err = part.Write(file.content)
		require.NoError(t, err)
		require.NoError(t, writer.WriteField("data", data))
		require.NoError(t, writer.Close())
		req, err := http.NewRequest("POST", "/upload", body)
		require.NoError(t, err)
		req.Header.Set("Content-Type", writer.FormDataContentType())
		req.Header.Set("Authorization", "Bearer "+bearer)
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)
		return w
	}
	multipartUpload := func(t *testing.T, data string, file updaterFeedFile) *httptest.ResponseRecorder {
		t.Helper()
		return multipartUploadAs(t, token, data, file)
	}
	// assertStoredUnderAdmin checks that a successful upload is recorded and stored under the admin, whoever sent it.
	assertStoredUnderAdmin := func(t *testing.T, w *httptest.ResponseRecorder, app, version, channel string, file updaterFeedFile) {
		t.Helper()
		doc := uploadedVersion(t, w)
		assert.Equal(t, "admin", doc.Owner)
		key := objectKey(app, version, channel, file.name)
		require.Len(t, doc.Artifacts, 1)
		assert.True(t, strings.HasSuffix(doc.Artifacts[0].Link, "/"+key), "link %s must point at %s", doc.Artifacts[0].Link, key)
		stat, err := uploader.StatObject(ctx, bucket, key)
		require.NoError(t, err, "object must be stored under the admin")
		md := md5.Sum(file.content)
		assert.Equal(t, hex.EncodeToString(md[:]), stat.MD5)
	}
	initOK := func(t *testing.T, data string, file updaterFeedFile) presignedInitResponse {
		t.Helper()
		w := doPresignedInit(t, router, token, data, presignedManifest([]updaterFeedFile{file}), nil)
		require.Equal(t, http.StatusOK, w.Code, "init: %s", w.Body.String())
		var resp presignedInitResponse
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
		status, body := putPresigned(t, resp, file.name, file.content)
		require.Less(t, status, 300, "PUT: %s", body)
		return resp
	}

	t.Run("multipart_rejects_app_outside_allowed_list", func(t *testing.T) {
		file := artifact(forbiddenApp, "1.0.0")
		w := multipartUpload(t, payload(forbiddenApp, "1.0.0", "nightly"), file)
		assert.Equal(t, http.StatusForbidden, w.Code, w.Body.String())
		assert.Contains(t, w.Body.String(), "access to this app")
		assertNotStored(t, objectKey(forbiddenApp, "1.0.0", "nightly", file.name))
	})

	t.Run("multipart_upload_within_allowed_lists", func(t *testing.T) {
		file := artifact(allowedApp, "0.9.0")
		w := multipartUpload(t, payload(allowedApp, "0.9.0", "nightly"), file)
		assertStoredUnderAdmin(t, w, allowedApp, "0.9.0", "nightly", file)
	})

	t.Run("api_token_multipart_upload", func(t *testing.T) {
		req, err := http.NewRequest("POST", "/token/create", strings.NewReader(fmt.Sprintf(`{"name":"%s","allowed_apps":["%s"]}`, apiTokenName, appID)))
		require.NoError(t, err)
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", "Bearer "+authToken)
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)
		require.Equal(t, http.StatusCreated, w.Code, "create token: %s", w.Body.String())
		var created map[string]interface{}
		require.NoError(t, json.Unmarshal(w.Body.Bytes(), &created))
		apiToken, _ := created["token"].(string)
		require.NotEmpty(t, apiToken)

		file := artifact(allowedApp, "0.9.1")
		w = multipartUploadAs(t, apiToken, payload(allowedApp, "0.9.1", "stable"), file)
		assertStoredUnderAdmin(t, w, allowedApp, "0.9.1", "stable", file)
	})

	t.Run("init_rejects_app_outside_allowed_list", func(t *testing.T) {
		file := artifact(forbiddenApp, "1.0.0")
		w := doPresignedInit(t, router, token, payload(forbiddenApp, "1.0.0", "nightly"), presignedManifest([]updaterFeedFile{file}), nil)
		assert.Equal(t, http.StatusForbidden, w.Code, w.Body.String())
		count, err := pendingUploads.CountDocuments(ctx, bson.M{"username": teamUser})
		require.NoError(t, err)
		assert.Zero(t, count, "a rejected init must not create a pending upload")
	})

	t.Run("complete_rechecks_app_access_and_releases", func(t *testing.T) {
		file := artifact(allowedApp, "1.0.0")
		upload := initOK(t, payload(allowedApp, "1.0.0", "nightly"), file)

		restore := revoke(t, "permissions.apps.allowed", appID)
		w := doPresignedComplete(t, router, token, upload.UploadID)
		restore()
		require.Equal(t, http.StatusForbidden, w.Code, w.Body.String())
		var pending struct {
			State string `bson:"state"`
		}
		require.NoError(t, pendingUploads.FindOne(ctx, bson.M{"_id": upload.UploadID}).Decode(&pending))
		assert.Equal(t, "pending", pending.State, "a denied complete must stay retryable")

		doc := uploadedVersion(t, doPresignedComplete(t, router, token, upload.UploadID))
		assert.Equal(t, "admin", doc.Owner, "a team user's upload is stored under their admin")
	})

	t.Run("init_rejects_entities_outside_allowed_lists", func(t *testing.T) {
		cases := []struct {
			field, id, message string
		}{
			{"permissions.channels.allowed", channelID, "access to this channel"},
			{"permissions.platforms.allowed", platformID, "access to this platform"},
			{"permissions.archs.allowed", archID, "access to this architecture"},
		}
		for _, tc := range cases {
			file := artifact(allowedApp, "1.1.0")
			restore := revoke(t, tc.field, tc.id)
			w := doPresignedInit(t, router, token, payload(allowedApp, "1.1.0", "nightly"), presignedManifest([]updaterFeedFile{file}), nil)
			restore()
			assert.Equal(t, http.StatusForbidden, w.Code, "%s: %s", tc.field, w.Body.String())
			assert.Contains(t, w.Body.String(), tc.message, tc.field)
		}
	})

	t.Run("complete_rechecks_channel_access_before_storage", func(t *testing.T) {
		file := artifact(allowedApp, "1.2.0")
		upload := initOK(t, payload(allowedApp, "1.2.0", "nightly"), file)

		restore := revoke(t, "permissions.channels.allowed", channelID)
		w := doPresignedComplete(t, router, token, upload.UploadID)
		restore()
		assert.Equal(t, http.StatusForbidden, w.Code, w.Body.String())
		assert.Contains(t, w.Body.String(), "access to this channel")
		assertNotStored(t, objectKey(allowedApp, "1.2.0", "nightly", file.name))
	})

	t.Run("multipart_rejects_channel_outside_allowed_list_before_storage", func(t *testing.T) {
		file := artifact(allowedApp, "1.3.0")
		restore := revoke(t, "permissions.channels.allowed", channelID)
		w := multipartUpload(t, payload(allowedApp, "1.3.0", "nightly"), file)
		restore()
		assert.Equal(t, http.StatusForbidden, w.Code, w.Body.String())
		assert.Contains(t, w.Body.String(), "access to this channel")
		assertNotStored(t, objectKey(allowedApp, "1.3.0", "nightly", file.name))
	})
}

// func TestSquirrelReleases(t *testing.T) {
// 	router := gin.Default()
// 	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
// 	router.GET("/update/:owner/:app/:channel/:platform/:arch/:version/RELEASES", func(c *gin.Context) {
// 		handler.SquirrelReleases(c)
// 	})

// 	// Define test scenarios for SquirrelReleases handler
// 	testScenarios := []struct {
// 		Owner        string
// 		AppName      string
// 		ChannelName  string
// 		Platform     string
// 		Arch         string
// 		Version      string
// 		ExpectedCode int
// 		TestName     string
// 		ExpectedBody string
// 	}{
// 		{
// 			Owner:        "admin",
// 			AppName:      "updaters",
// 			ChannelName:  "stable",
// 			Platform:     "windows",
// 			Arch:         "universalArch",
// 			Version:      "0.0.1.137",
// 			ExpectedCode: http.StatusFound, // 302 redirect
// 			TestName:     "SquirrelReleasesUpdateAvailable",
// 			ExpectedBody: "RELEASES FILE FOR WINDOWS sqirrel",
// 		},
// 		{
// 			Owner:        "admin",
// 			AppName:      "updaters",
// 			ChannelName:  "stable",
// 			Platform:     "windows",
// 			Arch:         "universalArch",
// 			Version:      "0.0.6.137",
// 			ExpectedCode: http.StatusFound, // 302 redirect
// 			TestName:     "SquirrelReleasesUpdateNotAvailable",
// 			ExpectedBody: "RELEASES FILE FOR WINDOWS sqirrel",
// 		},
// 		// {
// 		// 	Owner:        "admin",
// 		// 	AppName:      "nonexistent",
// 		// 	ChannelName:  "stable",
// 		// 	Platform:     "windows",
// 		// 	Arch:         "universalArch",
// 		// 	Version:      "0.0.1.137",
// 		// 	ExpectedCode: http.StatusBadRequest, // Should return error for non-existent app
// 		// 	TestName:     "SquirrelReleasesNonExistentApp",
// 		// },
// 	}

// 	// Run test scenarios
// 	for _, scenario := range testScenarios {
// 		t.Run(scenario.TestName, func(t *testing.T) {
// 			// Create URL with parameters
// 			url := fmt.Sprintf("/update/%s/%s/%s/%s/%s/%s/RELEASES",
// 				scenario.Owner,
// 				scenario.AppName,
// 				scenario.ChannelName,
// 				scenario.Platform,
// 				scenario.Arch,
// 				scenario.Version,
// 			)

// 			// Create request with proper host information
// 			req, err := http.NewRequest("GET", url, nil)
// 			if err != nil {
// 				t.Fatal(err)
// 			}

// 			// Create response recorder
// 			w := httptest.NewRecorder()

// 			// Serve the request
// 			router.ServeHTTP(w, req)

// 			// Log full response for debugging
// 			logrus.Infof("=== Test: %s ===", scenario.TestName)
// 			logrus.Infof("Status Code: %d", w.Code)
// 			logrus.Infof("Headers: %v", w.Header())
// 			logrus.Infof("Body: %s", w.Body.String())
// 			logrus.Infof("==================")

// 			// Check status code
// 			assert.Equal(t, scenario.ExpectedCode, w.Code, "Expected status code %d, got %d", scenario.ExpectedCode, w.Code)
// 			// For 302 Found (redirect), check the Location header and HTML body
// 			locationHeader := w.Header().Get("Location")
// 			logrus.Infof("Location Header: %s", locationHeader)
// 			if locationHeader != "" {
// 				// If Location header is set, that's the redirect URL
// 				// var expectedURL string
// 				// if url, exists := scenario.ExpectedJSON["url"]; exists {
// 				// 	expectedURL = url.(string)
// 				// } else if updateURL, exists := scenario.ExpectedJSON["update_url_yml"]; exists {
// 				// 	expectedURL = updateURL.(string)
// 				// } else {
// 				// 	t.Fatal("Neither 'url' nor 'update_url_yml' found in ExpectedJSON")
// 				// }
// 				// assert.Equal(t, expectedURL, locationHeader)
// 				body := checkFileContent(t, locationHeader)
// 				logrus.Infof("asserting that body: %s, contains: %s", body, scenario.ExpectedBody)
// 				assert.Contains(t, body, scenario.ExpectedBody)
// 			} else {
// 				// If no Location header, check HTML body for redirect link
// 				body := w.Body.String()
// 				logrus.Infof("Body in squirrel win releases: %s", body)
// 				// expectedURL := scenario.ExpectedJSON["url"].(string)
// 				// Check if the expected URL is present in the HTML body
// 				// assert.Contains(t, body, expectedURL)
// 			}
// 			// // For 302 redirects, check if Location header is present
// 			// if scenario.ExpectedCode == http.StatusFound {
// 			// 	locationHeader := w.Header().Get("Location")
// 			// 	assert.NotEmpty(t, locationHeader, "Location header should be present for 302 redirect")

// 			// 	// Log the redirect URL for debugging
// 			// 	logrus.Infof("Redirect URL for %s: %s", scenario.TestName, locationHeader)
// 			// }

// 			// // For error cases, check if error message is present
// 			// if scenario.ExpectedCode == http.StatusBadRequest {
// 			// 	var response map[string]interface{}
// 			// 	err := json.Unmarshal(w.Body.Bytes(), &response)
// 			// 	assert.NoError(t, err, "Should be able to parse error response as JSON")
// 			// 	assert.Contains(t, response, "error", "Error response should contain 'error' field")
// 			// }
// 		})
// 	}
// }

var cicdToken string
var cicdTokenID string
var cicdTokenSecondUserID string
var cicdTokenAdminAppID string
var cicdTokenSecondAdminAppID string

func TestTokenFlow01Create(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	appHandler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/token/create", utils.AdminOnlyMiddleware(mongoDatabase), func(c *gin.Context) {
		appHandler.CreateToken(c)
	})

	adminAppID := primitive.NewObjectID()
	secondAdminAppID := primitive.NewObjectID()
	collection := mongoDatabase.Collection("apps_meta")

	_, err := collection.InsertMany(context.TODO(), []interface{}{
		bson.M{"_id": adminAppID, "owner": "admin", "app_name": "token-flow-admin-app"},
		bson.M{"_id": secondAdminAppID, "owner": "administrator", "app_name": "token-flow-admin-second-app"},
	})
	assert.NoError(t, err)

	cicdTokenAdminAppID = adminAppID.Hex()
	cicdTokenSecondAdminAppID = secondAdminAppID.Hex()

	adminPayload := fmt.Sprintf(`{"name":"cicd-token-admin","allowed_apps":["%s"]}`, cicdTokenAdminAppID)
	adminReq, err := http.NewRequest("POST", "/token/create", bytes.NewBufferString(adminPayload))
	if err != nil {
		t.Fatal(err)
	}
	adminReq.Header.Set("Content-Type", "application/json")
	adminReq.Header.Set("Authorization", "Bearer "+authToken)
	adminW := httptest.NewRecorder()
	router.ServeHTTP(adminW, adminReq)
	assert.Equal(t, http.StatusCreated, adminW.Code)

	var adminResponse map[string]interface{}
	err = json.Unmarshal(adminW.Body.Bytes(), &adminResponse)
	if err != nil {
		t.Fatal(err)
	}

	tokenValue, tokenExists := adminResponse["token"]
	assert.True(t, tokenExists)
	var ok bool
	cicdToken, ok = tokenValue.(string)
	require.True(t, ok, adminW.Body.String())
	assert.NotEmpty(t, cicdToken)

	tokenIDValue, tokenIDExists := adminResponse["id"]
	assert.True(t, tokenIDExists)
	cicdTokenID = tokenIDValue.(string)
	assert.NotEmpty(t, cicdTokenID)

	secondPayload := fmt.Sprintf(`{"name":"cicd-token-second-admin","allowed_apps":["%s"]}`, cicdTokenSecondAdminAppID)
	secondReq, err := http.NewRequest("POST", "/token/create", bytes.NewBufferString(secondPayload))
	if err != nil {
		t.Fatal(err)
	}
	secondReq.Header.Set("Content-Type", "application/json")
	secondReq.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	secondW := httptest.NewRecorder()
	router.ServeHTTP(secondW, secondReq)
	assert.Equal(t, http.StatusCreated, secondW.Code)

	var secondResponse map[string]interface{}
	err = json.Unmarshal(secondW.Body.Bytes(), &secondResponse)
	if err != nil {
		t.Fatal(err)
	}

	secondTokenValue, secondTokenExists := secondResponse["token"]
	assert.True(t, secondTokenExists)
	secondToken, ok := secondTokenValue.(string)
	require.True(t, ok, secondW.Body.String())
	assert.NotEmpty(t, secondToken)
	assert.NoError(t, os.Setenv("cicdTokenSecondUser", secondToken))

	secondTokenIDValue, secondTokenIDExists := secondResponse["id"]
	assert.True(t, secondTokenIDExists)
	cicdTokenSecondUserID = secondTokenIDValue.(string)
	assert.NotEmpty(t, cicdTokenSecondUserID)
}

func TestTokenCreateWithPastExpirationDate(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	appHandler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/token/create", utils.AdminOnlyMiddleware(mongoDatabase), func(c *gin.Context) {
		appHandler.CreateToken(c)
	})

	adminPastExpiryAppID := primitive.NewObjectID()
	_, err := mongoDatabase.Collection("apps_meta").InsertOne(context.TODO(), bson.M{
		"_id":      adminPastExpiryAppID,
		"owner":    "admin",
		"app_name": "token-past-expiry-app",
	})
	assert.NoError(t, err)
	defer func() {
		_, cleanupErr := mongoDatabase.Collection("apps_meta").DeleteOne(context.TODO(), bson.M{"_id": adminPastExpiryAppID})
		assert.NoError(t, cleanupErr)
	}()

	pastTime := time.Now().Add(-1 * time.Minute).UTC().Format(time.RFC3339Nano)
	payload := fmt.Sprintf(`{"name":"token-with-past-expiry","allowed_apps":["%s"],"expires_at":"%s"}`, adminPastExpiryAppID.Hex(), pastTime)
	req, err := http.NewRequest("POST", "/token/create", bytes.NewBufferString(payload))
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+authToken)

	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusBadRequest, w.Code)
	assert.Equal(t, `{"error":"expires_at must be in the future"}`, w.Body.String())
}

func TestTokenExpiresImmediatelyAndReturnsUnauthorized(t *testing.T) {
	createRouter := gin.Default()
	createRouter.Use(utils.AuthMiddleware(mongoDatabase))

	appHandler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	createRouter.POST("/token/create", utils.AdminOnlyMiddleware(mongoDatabase), func(c *gin.Context) {
		appHandler.CreateToken(c)
	})

	adminShortExpiryAppID := primitive.NewObjectID()
	_, err := mongoDatabase.Collection("apps_meta").InsertOne(context.TODO(), bson.M{
		"_id":      adminShortExpiryAppID,
		"owner":    "admin",
		"app_name": "token-short-expiry-app",
	})
	assert.NoError(t, err)
	defer func() {
		_, cleanupErr := mongoDatabase.Collection("apps_meta").DeleteOne(context.TODO(), bson.M{"_id": adminShortExpiryAppID})
		assert.NoError(t, cleanupErr)
	}()

	expiresSoon := time.Now().Add(250 * time.Millisecond).UTC().Format(time.RFC3339Nano)
	createPayload := fmt.Sprintf(`{"name":"token-short-expiry","allowed_apps":["%s"],"expires_at":"%s"}`, adminShortExpiryAppID.Hex(), expiresSoon)
	createReq, err := http.NewRequest("POST", "/token/create", bytes.NewBufferString(createPayload))
	if err != nil {
		t.Fatal(err)
	}
	createReq.Header.Set("Content-Type", "application/json")
	createReq.Header.Set("Authorization", "Bearer "+authToken)

	createW := httptest.NewRecorder()
	createRouter.ServeHTTP(createW, createReq)
	assert.Equal(t, http.StatusCreated, createW.Code)

	var createResponse map[string]interface{}
	err = json.Unmarshal(createW.Body.Bytes(), &createResponse)
	if err != nil {
		t.Fatal(err)
	}
	shortLivedToken, ok := createResponse["token"].(string)
	assert.True(t, ok)
	assert.NotEmpty(t, shortLivedToken)

	shortLivedTokenID, ok := createResponse["id"].(string)
	assert.True(t, ok)
	defer func() {
		deleteRouter := gin.Default()
		deleteRouter.Use(utils.AuthMiddleware(mongoDatabase))
		deleteRouter.DELETE("/token/delete", utils.AdminOnlyMiddleware(mongoDatabase), func(c *gin.Context) {
			appHandler.DeleteToken(c)
		})

		deleteReq, deleteErr := http.NewRequest("DELETE", "/token/delete", bytes.NewBufferString(fmt.Sprintf(`{"id":"%s"}`, shortLivedTokenID)))
		assert.NoError(t, deleteErr)
		deleteReq.Header.Set("Content-Type", "application/json")
		deleteReq.Header.Set("Authorization", "Bearer "+authToken)
		deleteW := httptest.NewRecorder()
		deleteRouter.ServeHTTP(deleteW, deleteReq)
		assert.Equal(t, http.StatusOK, deleteW.Code, deleteW.Body.String())
	}()

	time.Sleep(500 * time.Millisecond)
	useRouter := gin.Default()
	useRouter.Use(utils.AuthMiddleware(mongoDatabase))
	useRouter.POST("/test/upload", utils.CheckPermission(utils.PermissionUpload, utils.ResourceApps, mongoDatabase), func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"result": "ok"})
	})

	useReq, err := http.NewRequest("POST", "/test/upload", nil)
	if err != nil {
		t.Fatal(err)
	}
	useReq.Header.Set("Authorization", "Bearer "+shortLivedToken)

	useW := httptest.NewRecorder()
	useRouter.ServeHTTP(useW, useReq)

	assert.Equal(t, http.StatusUnauthorized, useW.Code)
	assert.Equal(t, `{"error":"token expired"}`, useW.Body.String())
}

func TestTokenMiddlewareFlowForBothTokens(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	router.POST("/test/app/create", utils.CheckPermission(utils.PermissionCreate, utils.ResourceApps, mongoDatabase), func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"result": "ok"})
	})
	router.POST("/test/upload", utils.CheckPermission(utils.PermissionUpload, utils.ResourceApps, mongoDatabase), func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"result": "ok"})
	})
	router.GET("/test/token/list", utils.AdminOnlyMiddleware(mongoDatabase), func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"result": "ok"})
	})

	secondToken := os.Getenv("cicdTokenSecondUser")
	assert.NotEmpty(t, cicdToken)
	assert.NotEmpty(t, secondToken)

	testTokens := []struct {
		name  string
		token string
	}{
		{name: "first api token", token: cicdToken},
		{name: "second api token", token: secondToken},
	}

	for _, tokenCase := range testTokens {
		t.Run(tokenCase.name, func(t *testing.T) {
			createReq, err := http.NewRequest("POST", "/test/app/create", nil)
			if err != nil {
				t.Fatal(err)
			}
			createReq.Header.Set("Authorization", "Bearer "+tokenCase.token)
			createW := httptest.NewRecorder()
			router.ServeHTTP(createW, createReq)
			assert.Equal(t, http.StatusForbidden, createW.Code)
			var createResponse map[string]interface{}
			err = json.Unmarshal(createW.Body.Bytes(), &createResponse)
			if err != nil {
				t.Fatal(err)
			}
			assert.Equal(t, "API tokens are restricted to app upload", createResponse["error"])

			uploadReq, err := http.NewRequest("POST", "/test/upload", nil)
			if err != nil {
				t.Fatal(err)
			}
			uploadReq.Header.Set("Authorization", "Bearer "+tokenCase.token)
			uploadW := httptest.NewRecorder()
			router.ServeHTTP(uploadW, uploadReq)
			assert.Equal(t, http.StatusOK, uploadW.Code)

			adminOnlyReq, err := http.NewRequest("GET", "/test/token/list", nil)
			if err != nil {
				t.Fatal(err)
			}
			adminOnlyReq.Header.Set("Authorization", "Bearer "+tokenCase.token)
			adminOnlyW := httptest.NewRecorder()
			router.ServeHTTP(adminOnlyW, adminOnlyReq)
			assert.Equal(t, http.StatusForbidden, adminOnlyW.Code)
			var adminOnlyResponse map[string]interface{}
			err = json.Unmarshal(adminOnlyW.Body.Bytes(), &adminOnlyResponse)
			if err != nil {
				t.Fatal(err)
			}
			assert.Equal(t, "Admin JWT is required", adminOnlyResponse["error"])
		})
	}
}

func TestTokenFlow02List(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	appHandler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/token/list", utils.AdminOnlyMiddleware(mongoDatabase), func(c *gin.Context) {
		appHandler.ListTokens(c)
	})

	adminReq, err := http.NewRequest("GET", "/token/list", nil)
	if err != nil {
		t.Fatal(err)
	}
	adminReq.Header.Set("Authorization", "Bearer "+authToken)
	adminW := httptest.NewRecorder()
	router.ServeHTTP(adminW, adminReq)
	assert.Equal(t, http.StatusOK, adminW.Code)

	var adminResponse map[string]interface{}
	err = json.Unmarshal(adminW.Body.Bytes(), &adminResponse)
	if err != nil {
		t.Fatal(err)
	}

	adminTokens, ok := adminResponse["tokens"].([]interface{})
	assert.True(t, ok)
	adminTokenFound := false
	secondUserTokenVisibleForAdmin := false
	for _, token := range adminTokens {
		tokenMap, mapOk := token.(map[string]interface{})
		if !mapOk {
			continue
		}
		if tokenMap["id"] == cicdTokenID {
			adminTokenFound = true
		}
		if tokenMap["id"] == cicdTokenSecondUserID {
			secondUserTokenVisibleForAdmin = true
		}
	}
	assert.True(t, adminTokenFound)
	assert.False(t, secondUserTokenVisibleForAdmin)

	secondReq, err := http.NewRequest("GET", "/token/list", nil)
	if err != nil {
		t.Fatal(err)
	}
	secondReq.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	secondW := httptest.NewRecorder()
	router.ServeHTTP(secondW, secondReq)
	assert.Equal(t, http.StatusOK, secondW.Code)

	var secondResponse map[string]interface{}
	err = json.Unmarshal(secondW.Body.Bytes(), &secondResponse)
	if err != nil {
		t.Fatal(err)
	}

	secondTokens, ok := secondResponse["tokens"].([]interface{})
	assert.True(t, ok)
	secondTokenFound := false
	adminTokenVisibleForSecondUser := false
	for _, token := range secondTokens {
		tokenMap, mapOk := token.(map[string]interface{})
		if !mapOk {
			continue
		}
		if tokenMap["id"] == cicdTokenSecondUserID {
			secondTokenFound = true
		}
		if tokenMap["id"] == cicdTokenID {
			adminTokenVisibleForSecondUser = true
		}
	}
	assert.True(t, secondTokenFound)
	assert.False(t, adminTokenVisibleForSecondUser)
	assert.NotEmpty(t, os.Getenv("cicdTokenSecondUser"))
}

func TestTokenFlow03Delete(t *testing.T) {
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	appHandler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.DELETE("/token/delete", utils.AdminOnlyMiddleware(mongoDatabase), func(c *gin.Context) {
		appHandler.DeleteToken(c)
	})

	adminPayload := fmt.Sprintf(`{"id":"%s"}`, cicdTokenID)
	adminReq, err := http.NewRequest("DELETE", "/token/delete", bytes.NewBufferString(adminPayload))
	if err != nil {
		t.Fatal(err)
	}
	adminReq.Header.Set("Content-Type", "application/json")
	adminReq.Header.Set("Authorization", "Bearer "+authToken)
	adminW := httptest.NewRecorder()
	router.ServeHTTP(adminW, adminReq)
	assert.Equal(t, http.StatusOK, adminW.Code)

	secondPayload := fmt.Sprintf(`{"id":"%s"}`, cicdTokenSecondUserID)
	secondReq, err := http.NewRequest("DELETE", "/token/delete", bytes.NewBufferString(secondPayload))
	if err != nil {
		t.Fatal(err)
	}
	secondReq.Header.Set("Content-Type", "application/json")
	secondReq.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	secondW := httptest.NewRecorder()
	router.ServeHTTP(secondW, secondReq)
	assert.Equal(t, http.StatusOK, secondW.Code)

	idsForCleanup := []primitive.ObjectID{}
	if cicdTokenAdminAppID != "" {
		appID, parseErr := primitive.ObjectIDFromHex(cicdTokenAdminAppID)
		assert.NoError(t, parseErr)
		if parseErr == nil {
			idsForCleanup = append(idsForCleanup, appID)
		}
	}
	if cicdTokenSecondAdminAppID != "" {
		appID, parseErr := primitive.ObjectIDFromHex(cicdTokenSecondAdminAppID)
		assert.NoError(t, parseErr)
		if parseErr == nil {
			idsForCleanup = append(idsForCleanup, appID)
		}
	}
	if len(idsForCleanup) > 0 {
		_, err = mongoDatabase.Collection("apps_meta").DeleteMany(context.TODO(), bson.M{"_id": bson.M{"$in": idsForCleanup}})
		assert.NoError(t, err)
	}
}

func TestUpdateSpecificAppWithSecondUser(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	// Define the route for the update endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/apps/update", func(c *gin.Context) {
		handler.UpdateSpecificApp(c)
	})

	// Create a file to update (you can replace this with a test file path).
	filePaths := []string{"LICENSE", "LICENSE"}
	for _, filePath := range filePaths {
		file, err := os.Open(filePath)
		if err != nil {
			t.Fatal(err)
		}
		defer file.Close()

		combinations := []struct {
			ID          string
			AppVersion  string
			ChannelName string
			Published   bool
			Critical    bool
			Platform    string
			Arch        string
			Changelog   string
		}{
			{uploadedAppIDs[1], "0.0.2.137", "nightly", true, true, "universalPlatform", "universalArch", "### Changelog"},
		}

		// Iterate through the combinations and update the file for each combination.
		for _, combo := range combinations {
			w := httptest.NewRecorder()
			// Reset the request body for each iteration.
			body := &bytes.Buffer{}
			writer := multipart.NewWriter(body)
			part, err := writer.CreateFormFile("file", filepath.Base(filePath))
			if err != nil {
				t.Fatal(err)
			}
			_, err = io.Copy(part, file)
			if err != nil {
				t.Fatal(err)
			}
			// Create a POST request for the update endpoint with the current combination.
			dataPart, err := writer.CreateFormField("data")
			if err != nil {
				t.Fatal(err)
			}
			payload := fmt.Sprintf(`{"id": "%s", "app_name": "testapp", "version": "%s", "channel": "%s", "publish": %v, "critical": %v, "platform": "%s", "arch": "%s", "changelog": "%s"}`, combo.ID, combo.AppVersion, combo.ChannelName, combo.Published, combo.Critical, combo.Platform, combo.Arch, combo.Changelog)
			_, err = dataPart.Write([]byte(payload))
			if err != nil {
				t.Fatal(err)
			}

			// Close the writer to finalize the form
			err = writer.Close()
			if err != nil {
				t.Fatal(err)
			}
			// logrus.Infoln("Body: ", body)
			req, err := http.NewRequest("POST", "/apps/update", body)
			if err != nil {
				t.Fatal(err)
			}

			// Set the Content-Type header for multipart/form-data.
			req.Header.Set("Content-Type", writer.FormDataContentType())

			// Set the Authorization header.
			req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
			// Serve the request using the Gin router.
			router.ServeHTTP(w, req)
			// Check the response status code. Platform names are scoped to the owner, so the
			// first admin's platform does not exist for the second one.
			assert.Equal(t, http.StatusBadRequest, w.Code)

			expected := `{"error":"wrong name of platform. Platform does not exist"}`
			assert.Equal(t, expected, w.Body.String())
		}
	}
}

func TestUpdateSpecificApp(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	// Define the route for the update endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/apps/update", func(c *gin.Context) {
		handler.UpdateSpecificApp(c)
	})

	// Create a file to update (you can replace this with a test file path).
	filePaths := []string{"LICENSE", "LICENSE"}
	for _, filePath := range filePaths {
		file, err := os.Open(filePath)
		if err != nil {
			t.Fatal(err)
		}
		defer file.Close()

		combinations := []struct {
			ID          string
			AppVersion  string
			ChannelName string
			Published   bool
			Critical    bool
			Platform    string
			Arch        string
			Changelog   string
		}{
			{uploadedAppIDs[1], "0.0.2.137", "nightly", true, true, "universalPlatform", "universalArch", "### Changelog"},
		}

		// Iterate through the combinations and update the file for each combination.
		for _, combo := range combinations {
			w := httptest.NewRecorder()
			// Reset the request body for each iteration.
			body := &bytes.Buffer{}
			writer := multipart.NewWriter(body)
			// LICENSE is already stored for this version; re-sending it is rejected with 409
			// (TestUploadRejectsBeforeWriting), so only the metadata is updated here.
			// Create a POST request for the update endpoint with the current combination.
			dataPart, err := writer.CreateFormField("data")
			if err != nil {
				t.Fatal(err)
			}
			payload := fmt.Sprintf(`{"id": "%s", "app_name": "testapp", "version": "%s", "channel": "%s", "publish": %v, "critical": %v, "platform": "%s", "arch": "%s", "changelog": "%s"}`, combo.ID, combo.AppVersion, combo.ChannelName, combo.Published, combo.Critical, combo.Platform, combo.Arch, combo.Changelog)
			_, err = dataPart.Write([]byte(payload))
			if err != nil {
				t.Fatal(err)
			}

			// Close the writer to finalize the form
			err = writer.Close()
			if err != nil {
				t.Fatal(err)
			}
			// logrus.Infoln("Body: ", body)
			req, err := http.NewRequest("POST", "/apps/update", body)
			if err != nil {
				t.Fatal(err)
			}

			// Set the Content-Type header for multipart/form-data.
			req.Header.Set("Content-Type", writer.FormDataContentType())

			// Set the Authorization header.
			req.Header.Set("Authorization", "Bearer "+authToken)
			// Serve the request using the Gin router.
			router.ServeHTTP(w, req)
			// Check the response status code.
			assert.Equal(t, http.StatusOK, w.Code)

			// Check the response status code.
			assert.Equal(t, http.StatusOK, w.Code)

			expected := `{"updatedResult.Updated":true}`
			assert.Equal(t, expected, w.Body.String())
		}
	}
}

func TestSearch(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /search endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/search", func(c *gin.Context) {
		handler.GetAppByName(c)
	})

	// Create a POST request for the /search endpoint.
	req, err := http.NewRequest("GET", "/search?app_name=testapp", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)
	// Define the expected JSON response as a slice of AppInfo.
	type AppInfo struct {
		ID         string                              `json:"ID"`
		AppID      string                              `json:"AppID"`
		AppName    string                              `json:"AppName"`
		Version    string                              `json:"Version"`
		Channel    string                              `json:"Channel"`
		Published  bool                                `json:"Published"`
		Critical   bool                                `json:"Critical"`
		Artifacts  []model.SpecificArtifactsWithoutIDs `json:"Artifacts" bson:"artifacts"`
		Changelog  []model.Changelog                   `json:"Changelog" bson:"changelog"`
		Updated_at string                              `json:"Updated_at"`
	}
	type AppResponse struct {
		Items []AppInfo `json:"items"`
		Total int       `json:"total"`
		Page  int       `json:"page"`
		Limit int       `json:"limit"`
	}

	expected := []AppInfo{
		{
			AppName:   "testapp",
			Version:   "0.0.5.137",
			Channel:   "stable",
			Published: false,
			Critical:  false,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.5.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
		{
			AppName:   "testapp",
			Version:   "0.0.4.137",
			Channel:   "stable",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.4.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
		{
			AppName:   "testapp",
			Version:   "0.0.3.137",
			Channel:   "nightly",
			Published: false,
			Critical:  false,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.3.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
		{
			AppName:   "testapp",
			Version:   "0.0.2.137",
			Channel:   "nightly",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.2.137",
					Changes: "### Changelog",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
	}

	var actual AppResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}

	// Compare the relevant fields (AppName, Version, Channel, Changelog) for each item in the response.
	if len(actual.Items) != len(expected) {
		t.Fatalf("Expected %d apps but got %d", len(expected), len(actual.Items))
	}

	for i, expectedApp := range expected {
		assert.Equal(t, expectedApp.AppName, actual.Items[i].AppName)
		assert.Equal(t, expectedApp.Version, actual.Items[i].Version)
		assert.Equal(t, expectedApp.Channel, actual.Items[i].Channel)
		assert.Equal(t, expectedApp.Published, actual.Items[i].Published)

		if len(expectedApp.Artifacts) != len(actual.Items[i].Artifacts) {
			t.Fatalf("Expected %d artifacts for app %s with version %s but got %d", len(expectedApp.Artifacts), expectedApp.ID, expectedApp.Version, len(actual.Items[i].Artifacts))
		}
		for j, expectedArtifact := range expectedApp.Artifacts {
			assert.Equal(t, expectedArtifact.Platform, actual.Items[i].Artifacts[j].Platform)
			assert.Equal(t, expectedArtifact.Arch, actual.Items[i].Artifacts[j].Arch)
			assert.Equal(t, expectedArtifact.Package, actual.Items[i].Artifacts[j].Package)
		}

		if len(expectedApp.Changelog) != len(actual.Items[i].Changelog) {
			t.Fatalf("Expected %d changelog entries for app %s but got %d", len(expectedApp.Changelog), expectedApp.ID, len(actual.Items[i].Changelog))
		}
		for c, expectedChanges := range expectedApp.Changelog {
			assert.Equal(t, expectedChanges.Version, actual.Items[i].Changelog[c].Version)
			assert.Equal(t, expectedChanges.Changes, actual.Items[i].Changelog[c].Changes)
			assert.Equal(t, expectedChanges.Date, actual.Items[i].Changelog[c].Date)
		}
	}

	assert.Equal(t, 1, actual.Page)
	assert.Equal(t, 9, actual.Limit)
	assert.Equal(t, len(expected), actual.Total)
}

func TestFilterSearchWithChannel(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /search endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/search", func(c *gin.Context) {
		handler.GetAppByName(c)
	})

	// Create a POST request for the /search endpoint.
	req, err := http.NewRequest("GET", "/search?app_name=testapp&channel=nightly", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)
	fmt.Println(w.Body.String())
	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)
	// Define the expected JSON response as a slice of AppInfo.
	type AppInfo struct {
		ID         string                              `json:"ID"`
		AppID      string                              `json:"AppID"`
		AppName    string                              `json:"AppName"`
		Version    string                              `json:"Version"`
		Channel    string                              `json:"Channel"`
		Published  bool                                `json:"Published"`
		Critical   bool                                `json:"Critical"`
		Artifacts  []model.SpecificArtifactsWithoutIDs `json:"Artifacts" bson:"artifacts"`
		Changelog  []model.Changelog                   `json:"Changelog" bson:"changelog"`
		Updated_at string                              `json:"Updated_at"`
	}
	type AppResponse struct {
		Items []AppInfo `json:"items"`
		Total int       `json:"total"`
		Page  int       `json:"page"`
		Limit int       `json:"limit"`
	}

	expected := []AppInfo{
		{
			AppName:   "testapp",
			Version:   "0.0.3.137",
			Channel:   "nightly",
			Published: false,
			Critical:  false,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.3.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
		{
			AppName:   "testapp",
			Version:   "0.0.2.137",
			Channel:   "nightly",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.2.137",
					Changes: "### Changelog",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
	}

	var actual AppResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}

	// Compare the relevant fields (AppName, Version, Channel, Changelog) for each item in the response.
	if len(actual.Items) != len(expected) {
		t.Fatalf("Expected %d apps but got %d", len(expected), len(actual.Items))
	}

	for i, expectedApp := range expected {
		assert.Equal(t, expectedApp.AppName, actual.Items[i].AppName)
		assert.Equal(t, expectedApp.Version, actual.Items[i].Version)
		assert.Equal(t, expectedApp.Channel, actual.Items[i].Channel)
		assert.Equal(t, expectedApp.Published, actual.Items[i].Published)

		if len(expectedApp.Artifacts) != len(actual.Items[i].Artifacts) {
			t.Fatalf("Expected %d artifacts for app %s with version %s but got %d", len(expectedApp.Artifacts), expectedApp.ID, expectedApp.Version, len(actual.Items[i].Artifacts))
		}
		for j, expectedArtifact := range expectedApp.Artifacts {
			assert.Equal(t, expectedArtifact.Platform, actual.Items[i].Artifacts[j].Platform)
			assert.Equal(t, expectedArtifact.Arch, actual.Items[i].Artifacts[j].Arch)
			assert.Equal(t, expectedArtifact.Package, actual.Items[i].Artifacts[j].Package)
		}

		if len(expectedApp.Changelog) != len(actual.Items[i].Changelog) {
			t.Fatalf("Expected %d changelog entries for app %s but got %d", len(expectedApp.Changelog), expectedApp.ID, len(actual.Items[i].Changelog))
		}
		for c, expectedChanges := range expectedApp.Changelog {
			assert.Equal(t, expectedChanges.Version, actual.Items[i].Changelog[c].Version)
			assert.Equal(t, expectedChanges.Changes, actual.Items[i].Changelog[c].Changes)
			assert.Equal(t, expectedChanges.Date, actual.Items[i].Changelog[c].Date)
		}
	}

	assert.Equal(t, 1, actual.Page)
	assert.Equal(t, 9, actual.Limit)
	assert.Equal(t, len(expected), actual.Total)
}

func TestFilterSearchWithChannelAndPublished(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /search endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/search", func(c *gin.Context) {
		handler.GetAppByName(c)
	})

	// Create a POST request for the /search endpoint.
	req, err := http.NewRequest("GET", "/search?app_name=testapp&channel=stable&published=true", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)
	fmt.Println(w.Body.String())
	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)
	// Define the expected JSON response as a slice of AppInfo.
	type AppInfo struct {
		ID         string                              `json:"ID"`
		AppID      string                              `json:"AppID"`
		AppName    string                              `json:"AppName"`
		Version    string                              `json:"Version"`
		Channel    string                              `json:"Channel"`
		Published  bool                                `json:"Published"`
		Critical   bool                                `json:"Critical"`
		Artifacts  []model.SpecificArtifactsWithoutIDs `json:"Artifacts" bson:"artifacts"`
		Changelog  []model.Changelog                   `json:"Changelog" bson:"changelog"`
		Updated_at string                              `json:"Updated_at"`
	}
	type AppResponse struct {
		Items []AppInfo `json:"items"`
		Total int       `json:"total"`
		Page  int       `json:"page"`
		Limit int       `json:"limit"`
	}

	expected := []AppInfo{
		{
			AppName:   "testapp",
			Version:   "0.0.4.137",
			Channel:   "stable",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.4.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
	}

	var actual AppResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}

	// Compare the relevant fields (AppName, Version, Channel, Changelog) for each item in the response.
	if len(actual.Items) != len(expected) {
		t.Fatalf("Expected %d apps but got %d", len(expected), len(actual.Items))
	}

	for i, expectedApp := range expected {
		assert.Equal(t, expectedApp.AppName, actual.Items[i].AppName)
		assert.Equal(t, expectedApp.Version, actual.Items[i].Version)
		assert.Equal(t, expectedApp.Channel, actual.Items[i].Channel)
		assert.Equal(t, expectedApp.Published, actual.Items[i].Published)

		if len(expectedApp.Artifacts) != len(actual.Items[i].Artifacts) {
			t.Fatalf("Expected %d artifacts for app %s with version %s but got %d", len(expectedApp.Artifacts), expectedApp.ID, expectedApp.Version, len(actual.Items[i].Artifacts))
		}
		for j, expectedArtifact := range expectedApp.Artifacts {
			assert.Equal(t, expectedArtifact.Platform, actual.Items[i].Artifacts[j].Platform)
			assert.Equal(t, expectedArtifact.Arch, actual.Items[i].Artifacts[j].Arch)
			assert.Equal(t, expectedArtifact.Package, actual.Items[i].Artifacts[j].Package)
		}

		if len(expectedApp.Changelog) != len(actual.Items[i].Changelog) {
			t.Fatalf("Expected %d changelog entries for app %s but got %d", len(expectedApp.Changelog), expectedApp.ID, len(actual.Items[i].Changelog))
		}
		for c, expectedChanges := range expectedApp.Changelog {
			assert.Equal(t, expectedChanges.Version, actual.Items[i].Changelog[c].Version)
			assert.Equal(t, expectedChanges.Changes, actual.Items[i].Changelog[c].Changes)
			assert.Equal(t, expectedChanges.Date, actual.Items[i].Changelog[c].Date)
		}
	}

	assert.Equal(t, 1, actual.Page)
	assert.Equal(t, 9, actual.Limit)
	assert.Equal(t, len(expected), actual.Total)
}

func TestFilterSearchWithChannelAndPublishedAndCritical(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /search endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/search", func(c *gin.Context) {
		handler.GetAppByName(c)
	})

	// Create a POST request for the /search endpoint.
	req, err := http.NewRequest("GET", "/search?app_name=testapp&channel=stable&published=true&critical=true", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)
	fmt.Println(w.Body.String())
	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)
	// Define the expected JSON response as a slice of AppInfo.
	type AppInfo struct {
		ID         string                              `json:"ID"`
		AppID      string                              `json:"AppID"`
		AppName    string                              `json:"AppName"`
		Version    string                              `json:"Version"`
		Channel    string                              `json:"Channel"`
		Published  bool                                `json:"Published"`
		Critical   bool                                `json:"Critical"`
		Artifacts  []model.SpecificArtifactsWithoutIDs `json:"Artifacts" bson:"artifacts"`
		Changelog  []model.Changelog                   `json:"Changelog" bson:"changelog"`
		Updated_at string                              `json:"Updated_at"`
	}
	type AppResponse struct {
		Items []AppInfo `json:"items"`
		Total int       `json:"total"`
		Page  int       `json:"page"`
		Limit int       `json:"limit"`
	}

	expected := []AppInfo{
		{
			AppName:   "testapp",
			Version:   "0.0.4.137",
			Channel:   "stable",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.4.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
	}

	var actual AppResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}

	// Compare the relevant fields (AppName, Version, Channel, Changelog) for each item in the response.
	if len(actual.Items) != len(expected) {
		t.Fatalf("Expected %d apps but got %d", len(expected), len(actual.Items))
	}

	for i, expectedApp := range expected {
		assert.Equal(t, expectedApp.AppName, actual.Items[i].AppName)
		assert.Equal(t, expectedApp.Version, actual.Items[i].Version)
		assert.Equal(t, expectedApp.Channel, actual.Items[i].Channel)
		assert.Equal(t, expectedApp.Published, actual.Items[i].Published)

		if len(expectedApp.Artifacts) != len(actual.Items[i].Artifacts) {
			t.Fatalf("Expected %d artifacts for app %s with version %s but got %d", len(expectedApp.Artifacts), expectedApp.ID, expectedApp.Version, len(actual.Items[i].Artifacts))
		}
		for j, expectedArtifact := range expectedApp.Artifacts {
			assert.Equal(t, expectedArtifact.Platform, actual.Items[i].Artifacts[j].Platform)
			assert.Equal(t, expectedArtifact.Arch, actual.Items[i].Artifacts[j].Arch)
			assert.Equal(t, expectedArtifact.Package, actual.Items[i].Artifacts[j].Package)
		}

		if len(expectedApp.Changelog) != len(actual.Items[i].Changelog) {
			t.Fatalf("Expected %d changelog entries for app %s but got %d", len(expectedApp.Changelog), expectedApp.ID, len(actual.Items[i].Changelog))
		}
		for c, expectedChanges := range expectedApp.Changelog {
			assert.Equal(t, expectedChanges.Version, actual.Items[i].Changelog[c].Version)
			assert.Equal(t, expectedChanges.Changes, actual.Items[i].Changelog[c].Changes)
			assert.Equal(t, expectedChanges.Date, actual.Items[i].Changelog[c].Date)
		}
	}

	assert.Equal(t, 1, actual.Page)
	assert.Equal(t, 9, actual.Limit)
	assert.Equal(t, len(expected), actual.Total)
}

func TestFilterSearchWithChannelAndPublishedAndCriticalAndPlatform(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /search endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/search", func(c *gin.Context) {
		handler.GetAppByName(c)
	})

	// Create a POST request for the /search endpoint.
	req, err := http.NewRequest("GET", "/search?app_name=testapp&channel=nightly&published=true&critical=true&platform=universalPlatform", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)
	fmt.Println(w.Body.String())
	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)
	// Define the expected JSON response as a slice of AppInfo.
	type AppInfo struct {
		ID         string                              `json:"ID"`
		AppID      string                              `json:"AppID"`
		AppName    string                              `json:"AppName"`
		Version    string                              `json:"Version"`
		Channel    string                              `json:"Channel"`
		Published  bool                                `json:"Published"`
		Critical   bool                                `json:"Critical"`
		Artifacts  []model.SpecificArtifactsWithoutIDs `json:"Artifacts" bson:"artifacts"`
		Changelog  []model.Changelog                   `json:"Changelog" bson:"changelog"`
		Updated_at string                              `json:"Updated_at"`
	}
	type AppResponse struct {
		Items []AppInfo `json:"items"`
		Total int       `json:"total"`
		Page  int       `json:"page"`
		Limit int       `json:"limit"`
	}

	expected := []AppInfo{
		{
			AppName:   "testapp",
			Version:   "0.0.2.137",
			Channel:   "nightly",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.2.137",
					Changes: "### Changelog",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
	}

	var actual AppResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}

	// Compare the relevant fields (AppName, Version, Channel, Changelog) for each item in the response.
	if len(actual.Items) != len(expected) {
		t.Fatalf("Expected %d apps but got %d", len(expected), len(actual.Items))
	}

	for i, expectedApp := range expected {
		assert.Equal(t, expectedApp.AppName, actual.Items[i].AppName)
		assert.Equal(t, expectedApp.Version, actual.Items[i].Version)
		assert.Equal(t, expectedApp.Channel, actual.Items[i].Channel)
		assert.Equal(t, expectedApp.Published, actual.Items[i].Published)

		if len(expectedApp.Artifacts) != len(actual.Items[i].Artifacts) {
			t.Fatalf("Expected %d artifacts for app %s with version %s but got %d", len(expectedApp.Artifacts), expectedApp.ID, expectedApp.Version, len(actual.Items[i].Artifacts))
		}
		for j, expectedArtifact := range expectedApp.Artifacts {
			assert.Equal(t, expectedArtifact.Platform, actual.Items[i].Artifacts[j].Platform)
			assert.Equal(t, expectedArtifact.Arch, actual.Items[i].Artifacts[j].Arch)
			assert.Equal(t, expectedArtifact.Package, actual.Items[i].Artifacts[j].Package)
		}

		if len(expectedApp.Changelog) != len(actual.Items[i].Changelog) {
			t.Fatalf("Expected %d changelog entries for app %s but got %d", len(expectedApp.Changelog), expectedApp.ID, len(actual.Items[i].Changelog))
		}
		for c, expectedChanges := range expectedApp.Changelog {
			assert.Equal(t, expectedChanges.Version, actual.Items[i].Changelog[c].Version)
			assert.Equal(t, expectedChanges.Changes, actual.Items[i].Changelog[c].Changes)
			assert.Equal(t, expectedChanges.Date, actual.Items[i].Changelog[c].Date)
		}
	}

	assert.Equal(t, 1, actual.Page)
	assert.Equal(t, 9, actual.Limit)
	assert.Equal(t, len(expected), actual.Total)
}

func TestFilterSearchWithChannelAndPublishedAndCriticalAndPlatformAndArch(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /search endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/search", func(c *gin.Context) {
		handler.GetAppByName(c)
	})

	// Create a POST request for the /search endpoint.
	req, err := http.NewRequest("GET", "/search?app_name=testapp&channel=stable&published=true&critical=true&platform=universalPlatform&arch=universalArch", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)
	fmt.Println(w.Body.String())
	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)
	// Define the expected JSON response as a slice of AppInfo.
	type AppInfo struct {
		ID         string                              `json:"ID"`
		AppID      string                              `json:"AppID"`
		AppName    string                              `json:"AppName"`
		Version    string                              `json:"Version"`
		Channel    string                              `json:"Channel"`
		Published  bool                                `json:"Published"`
		Critical   bool                                `json:"Critical"`
		Artifacts  []model.SpecificArtifactsWithoutIDs `json:"Artifacts" bson:"artifacts"`
		Changelog  []model.Changelog                   `json:"Changelog" bson:"changelog"`
		Updated_at string                              `json:"Updated_at"`
	}
	type AppResponse struct {
		Items []AppInfo `json:"items"`
		Total int       `json:"total"`
		Page  int       `json:"page"`
		Limit int       `json:"limit"`
	}

	expected := []AppInfo{

		{
			AppName:   "testapp",
			Version:   "0.0.4.137",
			Channel:   "stable",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.4.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
	}

	var actual AppResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}

	// Compare the relevant fields (AppName, Version, Channel, Changelog) for each item in the response.
	if len(actual.Items) != len(expected) {
		t.Fatalf("Expected %d apps but got %d", len(expected), len(actual.Items))
	}

	for i, expectedApp := range expected {
		assert.Equal(t, expectedApp.AppName, actual.Items[i].AppName)
		assert.Equal(t, expectedApp.Version, actual.Items[i].Version)
		assert.Equal(t, expectedApp.Channel, actual.Items[i].Channel)
		assert.Equal(t, expectedApp.Published, actual.Items[i].Published)

		if len(expectedApp.Artifacts) != len(actual.Items[i].Artifacts) {
			t.Fatalf("Expected %d artifacts for app %s with version %s but got %d", len(expectedApp.Artifacts), expectedApp.ID, expectedApp.Version, len(actual.Items[i].Artifacts))
		}
		for j, expectedArtifact := range expectedApp.Artifacts {
			assert.Equal(t, expectedArtifact.Platform, actual.Items[i].Artifacts[j].Platform)
			assert.Equal(t, expectedArtifact.Arch, actual.Items[i].Artifacts[j].Arch)
			assert.Equal(t, expectedArtifact.Package, actual.Items[i].Artifacts[j].Package)
		}

		if len(expectedApp.Changelog) != len(actual.Items[i].Changelog) {
			t.Fatalf("Expected %d changelog entries for app %s but got %d", len(expectedApp.Changelog), expectedApp.ID, len(actual.Items[i].Changelog))
		}
		for c, expectedChanges := range expectedApp.Changelog {
			assert.Equal(t, expectedChanges.Version, actual.Items[i].Changelog[c].Version)
			assert.Equal(t, expectedChanges.Changes, actual.Items[i].Changelog[c].Changes)
			assert.Equal(t, expectedChanges.Date, actual.Items[i].Changelog[c].Date)
		}
	}

	assert.Equal(t, 1, actual.Page)
	assert.Equal(t, 9, actual.Limit)
	assert.Equal(t, len(expected), actual.Total)
}

func TestSearchOnlyPublished(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /search endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/search", func(c *gin.Context) {
		handler.GetAppByName(c)
	})

	// Create a POST request for the /search endpoint.
	req, err := http.NewRequest("GET", "/search?app_name=testapp&published=true", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)
	// Define the expected JSON response as a slice of AppInfo.
	type AppInfo struct {
		ID         string                              `json:"ID"`
		AppID      string                              `json:"AppID"`
		AppName    string                              `json:"AppName"`
		Version    string                              `json:"Version"`
		Channel    string                              `json:"Channel"`
		Published  bool                                `json:"Published"`
		Critical   bool                                `json:"Critical"`
		Artifacts  []model.SpecificArtifactsWithoutIDs `json:"Artifacts" bson:"artifacts"`
		Changelog  []model.Changelog                   `json:"Changelog" bson:"changelog"`
		Updated_at string                              `json:"Updated_at"`
	}
	type AppResponse struct {
		Items []AppInfo `json:"items"`
		Total int       `json:"total"`
		Page  int       `json:"page"`
		Limit int       `json:"limit"`
	}

	expected := []AppInfo{
		{
			AppName:   "testapp",
			Version:   "0.0.4.137",
			Channel:   "stable",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.4.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
		{
			AppName:   "testapp",
			Version:   "0.0.2.137",
			Channel:   "nightly",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.2.137",
					Changes: "### Changelog",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
	}

	var actual AppResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}

	// Compare the relevant fields (AppName, Version, Channel, Changelog) for each item in the response.
	if len(actual.Items) != len(expected) {
		t.Fatalf("Expected %d apps but got %d", len(expected), len(actual.Items))
	}

	for i, expectedApp := range expected {
		assert.Equal(t, expectedApp.AppName, actual.Items[i].AppName)
		assert.Equal(t, expectedApp.Version, actual.Items[i].Version)
		assert.Equal(t, expectedApp.Channel, actual.Items[i].Channel)
		assert.Equal(t, expectedApp.Published, actual.Items[i].Published)

		if len(expectedApp.Artifacts) != len(actual.Items[i].Artifacts) {
			t.Fatalf("Expected %d artifacts for app %s with version %s but got %d", len(expectedApp.Artifacts), expectedApp.ID, expectedApp.Version, len(actual.Items[i].Artifacts))
		}
		for j, expectedArtifact := range expectedApp.Artifacts {
			assert.Equal(t, expectedArtifact.Platform, actual.Items[i].Artifacts[j].Platform)
			assert.Equal(t, expectedArtifact.Arch, actual.Items[i].Artifacts[j].Arch)
			assert.Equal(t, expectedArtifact.Package, actual.Items[i].Artifacts[j].Package)
		}

		if len(expectedApp.Changelog) != len(actual.Items[i].Changelog) {
			t.Fatalf("Expected %d changelog entries for app %s but got %d", len(expectedApp.Changelog), expectedApp.ID, len(actual.Items[i].Changelog))
		}
		for c, expectedChanges := range expectedApp.Changelog {
			assert.Equal(t, expectedChanges.Version, actual.Items[i].Changelog[c].Version)
			assert.Equal(t, expectedChanges.Changes, actual.Items[i].Changelog[c].Changes)
			assert.Equal(t, expectedChanges.Date, actual.Items[i].Changelog[c].Date)
		}
	}

	assert.Equal(t, 1, actual.Page)
	assert.Equal(t, 9, actual.Limit)
	assert.Equal(t, len(expected), actual.Total)
}

func TestSearchOnlyCritical(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /search endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/search", func(c *gin.Context) {
		handler.GetAppByName(c)
	})

	// Create a POST request for the /search endpoint.
	req, err := http.NewRequest("GET", "/search?app_name=testapp&critical=true", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)
	// Define the expected JSON response as a slice of AppInfo.
	type AppInfo struct {
		ID         string                              `json:"ID"`
		AppID      string                              `json:"AppID"`
		AppName    string                              `json:"AppName"`
		Version    string                              `json:"Version"`
		Channel    string                              `json:"Channel"`
		Published  bool                                `json:"Published"`
		Critical   bool                                `json:"Critical"`
		Artifacts  []model.SpecificArtifactsWithoutIDs `json:"Artifacts" bson:"artifacts"`
		Changelog  []model.Changelog                   `json:"Changelog" bson:"changelog"`
		Updated_at string                              `json:"Updated_at"`
	}
	type AppResponse struct {
		Items []AppInfo `json:"items"`
		Total int       `json:"total"`
		Page  int       `json:"page"`
		Limit int       `json:"limit"`
	}

	expected := []AppInfo{
		{
			AppName:   "testapp",
			Version:   "0.0.4.137",
			Channel:   "stable",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.4.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
		{
			AppName:   "testapp",
			Version:   "0.0.2.137",
			Channel:   "nightly",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.2.137",
					Changes: "### Changelog",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
	}

	var actual AppResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}

	// Compare the relevant fields (AppName, Version, Channel, Changelog) for each item in the response.
	if len(actual.Items) != len(expected) {
		t.Fatalf("Expected %d apps but got %d", len(expected), len(actual.Items))
	}

	for i, expectedApp := range expected {
		assert.Equal(t, expectedApp.AppName, actual.Items[i].AppName)
		assert.Equal(t, expectedApp.Version, actual.Items[i].Version)
		assert.Equal(t, expectedApp.Channel, actual.Items[i].Channel)
		assert.Equal(t, expectedApp.Published, actual.Items[i].Published)

		if len(expectedApp.Artifacts) != len(actual.Items[i].Artifacts) {
			t.Fatalf("Expected %d artifacts for app %s with version %s but got %d", len(expectedApp.Artifacts), expectedApp.ID, expectedApp.Version, len(actual.Items[i].Artifacts))
		}
		for j, expectedArtifact := range expectedApp.Artifacts {
			assert.Equal(t, expectedArtifact.Platform, actual.Items[i].Artifacts[j].Platform)
			assert.Equal(t, expectedArtifact.Arch, actual.Items[i].Artifacts[j].Arch)
			assert.Equal(t, expectedArtifact.Package, actual.Items[i].Artifacts[j].Package)
		}

		if len(expectedApp.Changelog) != len(actual.Items[i].Changelog) {
			t.Fatalf("Expected %d changelog entries for app %s but got %d", len(expectedApp.Changelog), expectedApp.ID, len(actual.Items[i].Changelog))
		}
		for c, expectedChanges := range expectedApp.Changelog {
			assert.Equal(t, expectedChanges.Version, actual.Items[i].Changelog[c].Version)
			assert.Equal(t, expectedChanges.Changes, actual.Items[i].Changelog[c].Changes)
			assert.Equal(t, expectedChanges.Date, actual.Items[i].Changelog[c].Date)
		}
	}

	assert.Equal(t, 1, actual.Page)
	assert.Equal(t, 9, actual.Limit)
	assert.Equal(t, len(expected), actual.Total)
}

func TestSearchOnlyUniversalPlatform(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /search endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/search", func(c *gin.Context) {
		handler.GetAppByName(c)
	})

	// Create a POST request for the /search endpoint.
	req, err := http.NewRequest("GET", "/search?app_name=testapp&platform=universalPlatform", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)
	// Define the expected JSON response as a slice of AppInfo.
	type AppInfo struct {
		ID         string                              `json:"ID"`
		AppID      string                              `json:"AppID"`
		AppName    string                              `json:"AppName"`
		Version    string                              `json:"Version"`
		Channel    string                              `json:"Channel"`
		Published  bool                                `json:"Published"`
		Critical   bool                                `json:"Critical"`
		Artifacts  []model.SpecificArtifactsWithoutIDs `json:"Artifacts" bson:"artifacts"`
		Changelog  []model.Changelog                   `json:"Changelog" bson:"changelog"`
		Updated_at string                              `json:"Updated_at"`
	}
	type AppResponse struct {
		Items []AppInfo `json:"items"`
		Total int       `json:"total"`
		Page  int       `json:"page"`
		Limit int       `json:"limit"`
	}

	expected := []AppInfo{
		{
			AppName:   "testapp",
			Version:   "0.0.5.137",
			Channel:   "stable",
			Published: false,
			Critical:  false,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.5.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
		{
			AppName:   "testapp",
			Version:   "0.0.4.137",
			Channel:   "stable",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.4.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
		{
			AppName:   "testapp",
			Version:   "0.0.3.137",
			Channel:   "nightly",
			Published: false,
			Critical:  false,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.3.137",
					Changes: "",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
		{
			AppName:   "testapp",
			Version:   "0.0.2.137",
			Channel:   "nightly",
			Published: true,
			Critical:  true,
			Artifacts: []model.SpecificArtifactsWithoutIDs{
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".dmg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  ".pkg",
				},
				{
					Platform: "universalPlatform",
					Arch:     "universalArch",
					Package:  "",
				},
			},
			Changelog: []model.Changelog{
				{
					Version: "0.0.2.137",
					Changes: "### Changelog",
					Date:    time.Now().Format("2006-01-02"),
				},
			},
		},
	}

	var actual AppResponse
	err = json.Unmarshal(w.Body.Bytes(), &actual)
	if err != nil {
		t.Fatal(err)
	}

	// Compare the relevant fields (AppName, Version, Channel, Changelog) for each item in the response.
	if len(actual.Items) != len(expected) {
		t.Fatalf("Expected %d apps but got %d", len(expected), len(actual.Items))
	}

	for i, expectedApp := range expected {
		assert.Equal(t, expectedApp.AppName, actual.Items[i].AppName)
		assert.Equal(t, expectedApp.Version, actual.Items[i].Version)
		assert.Equal(t, expectedApp.Channel, actual.Items[i].Channel)
		assert.Equal(t, expectedApp.Published, actual.Items[i].Published)

		if len(expectedApp.Artifacts) != len(actual.Items[i].Artifacts) {
			t.Fatalf("Expected %d artifacts for app %s with version %s but got %d", len(expectedApp.Artifacts), expectedApp.ID, expectedApp.Version, len(actual.Items[i].Artifacts))
		}
		for j, expectedArtifact := range expectedApp.Artifacts {
			assert.Equal(t, expectedArtifact.Platform, actual.Items[i].Artifacts[j].Platform)
			assert.Equal(t, expectedArtifact.Arch, actual.Items[i].Artifacts[j].Arch)
			assert.Equal(t, expectedArtifact.Package, actual.Items[i].Artifacts[j].Package)
		}

		if len(expectedApp.Changelog) != len(actual.Items[i].Changelog) {
			t.Fatalf("Expected %d changelog entries for app %s but got %d", len(expectedApp.Changelog), expectedApp.ID, len(actual.Items[i].Changelog))
		}
		for c, expectedChanges := range expectedApp.Changelog {
			assert.Equal(t, expectedChanges.Version, actual.Items[i].Changelog[c].Version)
			assert.Equal(t, expectedChanges.Changes, actual.Items[i].Changelog[c].Changes)
			assert.Equal(t, expectedChanges.Date, actual.Items[i].Changelog[c].Date)
		}
	}

	assert.Equal(t, 1, actual.Page)
	assert.Equal(t, 9, actual.Limit)
	assert.Equal(t, len(expected), actual.Total)
}

func TestFetchkLatestVersionOfApp(t *testing.T) {
	router := gin.Default()
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/apps/latest", func(c *gin.Context) {
		handler.FetchLatestVersionOfApp(c)
	})
	// Define test scenarios.
	testScenarios := []struct {
		AppName      string
		ChannelName  string
		ExpectedJSON map[string]interface{}
		ExpectedCode int
		Platform     string
		Arch         string
		TestName     string
		Owner        string
	}{
		{
			AppName:     "testapp",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				"nightly": map[string]interface{}{
					"universalPlatform": map[string]interface{}{
						"universalArch": map[string]interface{}{
							"dmg": map[string]interface{}{
								"url":     fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.137.dmg"),
								"version": "0.0.2.137",
							},
							"pkg": map[string]interface{}{
								"url":     fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.137.pkg"),
								"version": "0.0.2.137",
							},
							"no-extension": map[string]interface{}{
								"url":     fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.137"),
								"version": "0.0.2.137",
							},
						},
					},
				},
			},
			ExpectedCode: http.StatusOK,
			Platform:     "universalPlatform",
			Arch:         "universalArch",
			TestName:     "NightlyUpdateAvailable",
			Owner:        "admin",
		},
		{
			AppName:     "testapp",
			ChannelName: "stable",
			ExpectedJSON: map[string]interface{}{
				"stable": map[string]interface{}{
					"universalPlatform": map[string]interface{}{
						"universalArch": map[string]interface{}{
							"dmg": map[string]interface{}{
								"url":     fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.137.dmg"),
								"version": "0.0.4.137",
							},
							"pkg": map[string]interface{}{
								"url":     fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.137.pkg"),
								"version": "0.0.4.137",
							},
							"no-extension": map[string]interface{}{
								"url":     fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.137"),
								"version": "0.0.4.137",
							},
						},
					},
				},
			},
			ExpectedCode: http.StatusOK,
			Platform:     "universalPlatform",
			Arch:         "universalArch",
			TestName:     "StableUpdateAvailable",
			Owner:        "admin",
		},
	}

	for _, scenario := range testScenarios {
		t.Run(scenario.TestName, func(t *testing.T) {
			w := httptest.NewRecorder()

			// Create a GET request for checking the version.
			req, err := http.NewRequest("GET", fmt.Sprintf("/apps/latest?app_name=%s&channel=%s&platform=%s&arch=%s&owner=%s", scenario.AppName, scenario.ChannelName, scenario.Platform, scenario.Arch, scenario.Owner), nil)
			if err != nil {
				t.Fatal(err)
			}
			req.Header.Set(utils.DownloadTokenHeader, testappDownloadToken(t, scenario.ChannelName))

			// Serve the request using the Gin router.
			router.ServeHTTP(w, req)

			// Check the response status code.
			assert.Equal(t, scenario.ExpectedCode, w.Code)

			var actual map[string]interface{}
			err = json.Unmarshal(w.Body.Bytes(), &actual)
			if err != nil {
				t.Fatal(err)
			}

			// Compare the response with the expected values.
			assert.Equal(t, scenario.ExpectedJSON, actual)
		})
	}
}
func TestCheckVersion(t *testing.T) {
	router := gin.Default()
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/checkVersion", func(c *gin.Context) {
		handler.FindLatestVersion(c)
	})
	server := httptest.NewServer(router)
	defer server.Close()

	sdkClient := faynosync.NewClient(faynosync.Config{
		EdgeURL: s3Endpoint,
		BaseURL: server.URL,
	})

	decodeScenarioValue := func(raw string) string {
		decoded, err := url.QueryUnescape(raw)
		if err != nil {
			return raw
		}
		return decoded
	}

	// Define test scenarios.
	testScenarios := []struct {
		AppName      string
		Version      string
		ChannelName  string
		ExpectedJSON map[string]interface{}
		ExpectedCode int
		Published    bool
		Platform     string
		Arch         string
		TestName     string
		Owner        string
	}{
		{
			AppName:     "public%20testapp",
			Version:     "0.0.1.137",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				// "changelog":        "### Changelog\n",
				"update_available":  false,
				"critical":          false,
				"possible_rollback": false,
				"update_url_dmg":    fmt.Sprintf("%s/%s", s3Endpoint, "public%20testapp-admin/nightly/universalPlatform/universalArch/public%20testapp-0.0.1.137.dmg"),
				"update_url_pkg":    fmt.Sprintf("%s/%s", s3Endpoint, "public%20testapp-admin/nightly/universalPlatform/universalArch/public%20testapp-0.0.1.137.pkg"),
				"update_url":        fmt.Sprintf("%s/%s", s3Endpoint, "public%20testapp-admin/nightly/universalPlatform/universalArch/public%20testapp-0.0.1.137"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "universalPlatform",
			Arch:         "universalArch",
			TestName:     "NightlyUpdateAvailable",
			Owner:        "admin",
		},
		{
			AppName:     "testapp",
			Version:     "0.0.2.137",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				"update_available":  false,
				"critical":          false,
				"possible_rollback": false,
				"update_url_dmg":    fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.137.dmg"),
				"update_url_pkg":    fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.137.pkg"),
				"update_url":        fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.137"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "universalPlatform",
			Arch:         "universalArch",
			TestName:     "NightlyUpdateAvailable",
			Owner:        "admin",
		},
		{
			AppName:     "testapp",
			Version:     "0.0.3.137",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				"critical":          true,
				"possible_rollback": true,
				"changelog":         "### Changelog\n",
				"update_available":  false,
				"update_url_dmg":    fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.137.dmg"),
				"update_url_pkg":    fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.137.pkg"),
				"update_url":        fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.137"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "universalPlatform",
			Arch:         "universalArch",
			TestName:     "NightlyUpdateAvailable",
			Owner:        "admin",
		},
		{
			AppName:     "testapp",
			Version:     "0.0.4.137",
			ChannelName: "stable",
			ExpectedJSON: map[string]interface{}{
				"update_available":  false,
				"critical":          false,
				"possible_rollback": false,
				"update_url_dmg":    fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.137.dmg"),
				"update_url_pkg":    fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.137.pkg"),
				"update_url":        fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.137"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "universalPlatform",
			Arch:         "universalArch",
			TestName:     "StableUpdateAvailable",
			Owner:        "admin",
		},
		{
			AppName:     "testapp",
			Version:     "0.0.5.137",
			ChannelName: "stable",
			ExpectedJSON: map[string]interface{}{
				"update_available":  false,
				"critical":          true,
				"possible_rollback": true,
				"update_url_dmg":    fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.137.dmg"),
				"update_url_pkg":    fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.137.pkg"),
				"update_url":        fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.137"),
			},
			ExpectedCode: http.StatusOK,
			// Published:    false,
			Platform: "universalPlatform",
			Arch:     "universalArch",
			TestName: "StableUpdateAvailable",
			Owner:    "admin",
		},
	}

	for _, scenario := range testScenarios {
		t.Run(scenario.TestName, func(t *testing.T) {
			w := httptest.NewRecorder()

			// Create a GET request for checking the version.
			req, err := http.NewRequest("GET", fmt.Sprintf("/checkVersion?app_name=%s&version=%s&channel=%s&platform=%s&arch=%s&owner=%s", scenario.AppName, scenario.Version, scenario.ChannelName, scenario.Platform, scenario.Arch, scenario.Owner), nil)
			if err != nil {
				t.Fatal(err)
			}
			req.Header.Set(utils.DownloadTokenHeader, testappDownloadToken(t, scenario.ChannelName))

			// Serve the request using the Gin router.
			router.ServeHTTP(w, req)

			// Check the response status code.
			assert.Equal(t, scenario.ExpectedCode, w.Code)

			var actual map[string]interface{}
			err = json.Unmarshal(w.Body.Bytes(), &actual)
			if err != nil {
				t.Fatal(err)
			}

			// Compare the response with the expected values.
			assert.Equal(t, scenario.ExpectedJSON, actual)

			checkOpts := faynosync.CheckOptions{
				Owner:    decodeScenarioValue(scenario.Owner),
				AppName:  decodeScenarioValue(scenario.AppName),
				Version:  decodeScenarioValue(scenario.Version),
				Channel:  decodeScenarioValue(scenario.ChannelName),
				Platform: decodeScenarioValue(scenario.Platform),
				Arch:     decodeScenarioValue(scenario.Arch),
				DownloadToken: scenarioDownloadToken(t, decodeScenarioValue(scenario.AppName),
					decodeScenarioValue(scenario.ChannelName)),
			}
			expectedSource := faynosync.SourceAPI
			if scenario.AppName == "public%20testapp" && scenario.Version == "0.0.1.137" && scenario.ChannelName == "nightly" {
				expectedSource = faynosync.SourceEdge
				// The API publishes the CDN copy in the background, so the edge serves it shortly after the response.
				require.Eventually(t, func() bool {
					resp, err := sdkClient.CheckForUpdates(context.Background(), checkOpts)
					return err == nil && resp.Source == faynosync.SourceEdge
				}, 5*time.Second, 50*time.Millisecond)
			}
			sdkResp, err := sdkClient.CheckForUpdates(context.Background(), checkOpts)
			require.NoError(t, err)
			assertSDKMatchesScenario(t, scenario.ExpectedJSON, sdkResp)
			require.Equal(t, expectedSource, sdkResp.Source)
		})
	}
}

func TestUpdateSpecificAppWithCDNPublishFalseToCheckS3ObjectDeleted(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	// Define the route for the update endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/apps/update", func(c *gin.Context) {
		handler.UpdateSpecificApp(c)
	})

	// Create a file to update (you can replace this with a test file path).
	filePaths := []string{"LICENSE", "LICENSE"}
	for _, filePath := range filePaths {
		file, err := os.Open(filePath)
		if err != nil {
			t.Fatal(err)
		}
		defer file.Close()

		combinations := []struct {
			ID          string
			AppVersion  string
			ChannelName string
			Published   bool
			Critical    bool
			Platform    string
			Arch        string
			Changelog   string
		}{
			{uploadedAppIDs[0], "0.0.1.137", "nightly", false, true, "universalPlatform", "universalArch", "### Changelog"},
		}

		// Iterate through the combinations and update the file for each combination.
		for _, combo := range combinations {
			w := httptest.NewRecorder()
			// Reset the request body for each iteration.
			body := &bytes.Buffer{}
			writer := multipart.NewWriter(body)
			// LICENSE is already stored for this version; re-sending it is rejected with 409
			// (TestUploadRejectsBeforeWriting), so only the metadata is updated here.
			// Create a POST request for the update endpoint with the current combination.
			dataPart, err := writer.CreateFormField("data")
			if err != nil {
				t.Fatal(err)
			}
			payload := fmt.Sprintf(`{"id": "%s", "app_name": "public testapp", "version": "%s", "channel": "%s", "publish": %v, "critical": %v, "platform": "%s", "arch": "%s", "changelog": "%s"}`, combo.ID, combo.AppVersion, combo.ChannelName, combo.Published, combo.Critical, combo.Platform, combo.Arch, combo.Changelog)
			_, err = dataPart.Write([]byte(payload))
			if err != nil {
				t.Fatal(err)
			}

			// Close the writer to finalize the form
			err = writer.Close()
			if err != nil {
				t.Fatal(err)
			}
			// logrus.Infoln("Body: ", body)
			req, err := http.NewRequest("POST", "/apps/update", body)
			if err != nil {
				t.Fatal(err)
			}

			// Set the Content-Type header for multipart/form-data.
			req.Header.Set("Content-Type", writer.FormDataContentType())

			// Set the Authorization header.
			req.Header.Set("Authorization", "Bearer "+authToken)
			// Serve the request using the Gin router.
			router.ServeHTTP(w, req)
			assert.Equal(t, http.StatusOK, w.Code)

			expected := `{"updatedResult.Updated":true}`
			assert.Equal(t, expected, w.Body.String())

			cdnResponseURL, err := url.Parse(s3Endpoint)
			require.NoError(t, err)
			cdnResponseURL = cdnResponseURL.JoinPath(
				"responses",
				"admin",
				"public testapp",
				combo.ChannelName,
				combo.Platform,
				combo.Arch,
				combo.AppVersion+".json",
			)

			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
			defer cancel()
			cdnReq, err := http.NewRequestWithContext(ctx, http.MethodGet, cdnResponseURL.String(), nil)
			require.NoError(t, err)
			resp, err := http.DefaultClient.Do(cdnReq)
			require.NoError(t, err)
			defer resp.Body.Close()
			// Providers that keep s3:ListBucket private answer a missing key with 403 instead of 404.
			assert.Contains(t, []int{http.StatusNotFound, http.StatusForbidden}, resp.StatusCode, "CDN response JSON should be deleted after publish=false update: %s", cdnResponseURL.String())
		}
	}
}

// bulkDeleteQuery builds the "?id=a&id=b" form of the delete request.
func bulkDeleteQuery(appIDs []string) string {
	return "/apps/delete?id=" + strings.Join(appIDs, "&id=")
}

func TestMultipleDeleteOfDifferentApps(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /apps/delete endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.DELETE("/apps/delete", func(c *gin.Context) {
		handler.DeleteSpecificVersionOfApp(c)
	})

	// uploadedAppIDs[0] is a version of "public testapp", the rest belong to
	// "testapp", and one request may only touch a single application.
	req, err := http.NewRequest("DELETE", bulkDeleteQuery([]string{uploadedAppIDs[0], uploadedAppIDs[1]}), nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusBadRequest, w.Code)

	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	assert.Equal(t, "all versions must belong to the same application", response["error"])
	assert.ElementsMatch(t, []interface{}{"public testapp", "testapp"}, response["apps"])
}

func TestMultipleDeleteWithSecondUser(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /apps/delete endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.DELETE("/apps/delete", func(c *gin.Context) {
		handler.DeleteSpecificVersionOfApp(c)
	})

	req, err := http.NewRequest("DELETE", bulkDeleteQuery([]string{uploadedAppIDs[1], uploadedAppIDs[2]}), nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusForbidden, w.Code)

	expected := fmt.Sprintf(`{"error":"you don't have permission to delete these items","forbidden":["%s","%s"]}`, uploadedAppIDs[1], uploadedAppIDs[2])
	assert.Equal(t, expected, w.Body.String())
}

func TestMultipleDelete(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /apps/delete endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.DELETE("/apps/delete", func(c *gin.Context) {
		handler.DeleteSpecificVersionOfApp(c)
	})

	// Every version of "testapp" goes away in a single request. The rejected
	// requests above must have left all of them in place.
	publicAppID, testAppIDs := uploadedAppIDs[0], uploadedAppIDs[1:]

	req, err := http.NewRequest("DELETE", bulkDeleteQuery(testAppIDs), nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authToken)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	assert.Equal(t, float64(len(testAppIDs)), response["deleteSpecificAppResult.DeletedCount"])
	assert.Equal(t, "testapp", response["app_name"])
	assert.Nil(t, response["orphaned_links"])

	deleted, deletedExists := response["deleted"].([]interface{})
	assert.True(t, deletedExists)

	var deletedIDs []string
	for _, item := range deleted {
		version, ok := item.(map[string]interface{})
		assert.True(t, ok)
		deletedIDs = append(deletedIDs, version["id"].(string))
	}
	assert.ElementsMatch(t, testAppIDs, deletedIDs)

	// A single id keeps the original response of this endpoint.
	w = httptest.NewRecorder()
	req, err = http.NewRequest("DELETE", "/apps/delete?id="+publicAppID, nil)
	if err != nil {
		t.Fatal(err)
	}

	req.Header.Set("Authorization", "Bearer "+authToken)
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"deleteSpecificAppResult.DeletedCount":1}`
	assert.Equal(t, expected, w.Body.String())
}

func TestMultipleDeleteWithUpdaters(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	// Define the route for the /apps/delete endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.DELETE("/apps/delete", func(c *gin.Context) {
		handler.DeleteSpecificVersionOfApp(c)
	})

	// Iterate over the uploadedAppIDs and send a DELETE request for each ID.
	for _, appID := range uploadedAppIDsWithUpdaters {
		w := httptest.NewRecorder()

		req, err := http.NewRequest("DELETE", "/apps/delete?id="+appID, nil)
		if err != nil {
			t.Fatal(err)
		}

		// Set the Authorization header.
		req.Header.Set("Authorization", "Bearer "+authToken)
		// Serve the request using the Gin router.
		router.ServeHTTP(w, req)

		// Check the response status code for each request.
		assert.Equal(t, http.StatusOK, w.Code)

		expected := `{"deleteSpecificAppResult.DeletedCount":1}`
		assert.Equal(t, expected, w.Body.String())
	}
}

var uploadedAppIDsWithSameExtension []string

func TestMultipleUploadWithSameExtension(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	// Define the route for the upload endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/upload", func(c *gin.Context) {
		handler.UploadApp(c)
	})

	// Create a file to upload (you can replace this with a test file path).
	filePaths := []string{"testapp.dmg"}
	for _, filePath := range filePaths {
		file, err := os.Open(filePath)
		if err != nil {
			t.Fatal(err)
		}
		defer file.Close()

		combinations := []struct {
			AppVersion  string
			ChannelName string
			Published   bool
			Critical    bool
			Platform    string
			Arch        string
		}{
			{"0.0.1.138", "nightly", true, false, "secondPlatform", "secondArch"},
			{"0.0.2.138", "nightly", true, false, "universalPlatform", "secondArch"},
			{"0.0.2.138", "nightly", true, false, "universalPlatform", "universalArch"},
			{"0.0.4.138", "stable", true, true, "universalPlatform", "universalArch"},
			{"0.0.4.138", "stable", true, true, "secondPlatform", "universalArch"},
		}
		logrus.Infof("Initial combinations: %v", combinations)
		// Iterate through the combinations and upload the file for each combination.
		for _, combo := range combinations {
			logrus.Infof("Uploading payload for: %v", combo)
			w := httptest.NewRecorder()
			// Reset the request body for each iteration.
			body := &bytes.Buffer{}
			writer := multipart.NewWriter(body)
			part, err := writer.CreateFormFile("file", filepath.Base(filePath))
			if err != nil {
				t.Fatal(err)
			}
			_, err = io.Copy(part, file)
			if err != nil {
				t.Fatal(err)
			}
			dataPart, err := writer.CreateFormField("data")
			if err != nil {
				t.Fatal(err)
			}
			payload := fmt.Sprintf(`{"app_name": "testapp", "version": "%s", "channel": "%s", "publish": %v, "critical": %v, "platform": "%s", "arch": "%s"}`, combo.AppVersion, combo.ChannelName, combo.Published, combo.Critical, combo.Platform, combo.Arch)
			_, err = dataPart.Write([]byte(payload))
			if err != nil {
				t.Fatal(err)
			}

			// Close the writer to finalize the form
			err = writer.Close()
			if err != nil {
				t.Fatal(err)
			}
			logrus.Infoln("Payload: ", payload)
			// Create a POST request for the /upload endpoint with the current combination.
			req, err := http.NewRequest("POST", "/upload", body)
			if err != nil {
				t.Fatal(err)
			}

			// Set the Content-Type header for multipart/form-data.
			req.Header.Set("Content-Type", writer.FormDataContentType())

			// Set the Authorization header.
			req.Header.Set("Authorization", "Bearer "+authToken)
			// Serve the request using the Gin router.
			router.ServeHTTP(w, req)

			// Check the response status code.
			assert.Equal(t, http.StatusOK, w.Code)
			var response map[string]interface{}
			err = json.Unmarshal(w.Body.Bytes(), &response)
			if err != nil {
				t.Fatal(err)
			}

			id, idExists := response["uploadResult.Uploaded"]
			assert.True(t, idExists)

			// Check if the id already exists in the uploadedAppIDsWithSameExtension array
			exists := false
			for _, val := range uploadedAppIDsWithSameExtension {
				if val == id {
					exists = true
					break
				}
			}

			// If id does not exist in the array, append it
			if !exists {
				uploadedAppIDsWithSameExtension = append(uploadedAppIDsWithSameExtension, id.(string))
			}

			assert.True(t, idExists)
			assert.NotEmpty(t, id.(string))
		}
	}
}

func TestCheckVersionWithSameExtensionArtifactsAndDiffPlatformsArchs(t *testing.T) {
	router := gin.Default()
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/checkVersion", func(c *gin.Context) {
		handler.FindLatestVersion(c)
	})
	server := httptest.NewServer(router)
	defer server.Close()

	sdkClient := faynosync.NewClient(faynosync.Config{
		BaseURL: server.URL,
	})

	decodeScenarioValue := func(raw string) string {
		decoded, err := url.QueryUnescape(raw)
		if err != nil {
			return raw
		}
		return decoded
	}
	// Define test scenarios.
	testScenarios := []struct {
		AppName      string
		Version      string
		ChannelName  string
		ExpectedJSON map[string]interface{}
		ExpectedCode int
		Published    bool
		Platform     string
		Arch         string
		TestName     string
		Owner        string
		DeviceID     string
	}{
		{
			AppName:     "testapp",
			Version:     "0.0.1.138",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				"update_available": true,
				"critical":         false,
				"update_url_dmg":   fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FsecondArch%2Ftestapp-0.0.2.138.dmg"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "universalPlatform",
			Arch:         "secondArch",
			TestName:     "NightlyUpdateAvailable",
			Owner:        "admin",
			DeviceID:     "device-001",
		},
		{
			AppName:     "testapp",
			Version:     "0.0.1.138",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				"update_available": true,
				"critical":         false,
				"update_url_dmg":   fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.2.138.dmg"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "universalPlatform",
			Arch:         "universalArch",
			TestName:     "NightlyUpdateAvailable",
			Owner:        "admin",
			DeviceID:     "device-002",
		},
		{
			AppName:     "testapp",
			Version:     "0.0.3.138",
			ChannelName: "nightly",
			ExpectedJSON: map[string]interface{}{
				"critical":          false,
				"possible_rollback": true,
				"update_available":  false,
				"update_url_dmg":    fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fnightly%2FsecondPlatform%2FsecondArch%2Ftestapp-0.0.1.138.dmg"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "secondPlatform",
			Arch:         "secondArch",
			TestName:     "NightlyUpdateAvailable",
			Owner:        "admin",
			DeviceID:     "device-003",
		},
		{
			AppName:     "testapp",
			Version:     "0.0.3.138",
			ChannelName: "stable",
			ExpectedJSON: map[string]interface{}{
				"update_available": true,
				"critical":         true,
				"update_url_dmg":   fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FuniversalPlatform%2FuniversalArch%2Ftestapp-0.0.4.138.dmg"),
			},
			ExpectedCode: http.StatusOK,
			Platform:     "universalPlatform",
			Arch:         "universalArch",
			TestName:     "StableUpdateAvailable",
			Owner:        "admin",
			DeviceID:     "device-004",
		},
		{
			AppName:     "testapp",
			Version:     "0.0.3.138",
			ChannelName: "stable",
			ExpectedJSON: map[string]interface{}{
				"update_available": true,
				"critical":         true,
				"update_url_dmg":   fmt.Sprintf("%s/%s%s", apiUrl, "download?key=", "testapp-admin%2Fstable%2FsecondPlatform%2FuniversalArch%2Ftestapp-0.0.4.138.dmg"),
			},
			ExpectedCode: http.StatusOK,
			// Published:    false,
			Platform: "secondPlatform",
			Arch:     "universalArch",
			TestName: "StableUpdateAvailable",
			Owner:    "admin",
			DeviceID: "device-005",
		},
	}

	for _, scenario := range testScenarios {
		t.Run(scenario.TestName, func(t *testing.T) {
			w := httptest.NewRecorder()

			// Create a GET request for checking the version.
			req, err := http.NewRequest("GET", fmt.Sprintf("/checkVersion?app_name=%s&version=%s&channel=%s&platform=%s&arch=%s&owner=%s", scenario.AppName, scenario.Version, scenario.ChannelName, scenario.Platform, scenario.Arch, scenario.Owner), nil)
			if err != nil {
				t.Fatal(err)
			}
			req.Header.Set(utils.DownloadTokenHeader, testappDownloadToken(t, scenario.ChannelName))
			req.Header.Set("X-Device-ID", scenario.DeviceID)
			// Serve the request using the Gin router.
			router.ServeHTTP(w, req)

			// Check the response status code.
			assert.Equal(t, scenario.ExpectedCode, w.Code)

			var actual map[string]interface{}
			err = json.Unmarshal(w.Body.Bytes(), &actual)
			if err != nil {
				t.Fatal(err)
			}

			// Compare the response with the expected values.
			assert.Equal(t, scenario.ExpectedJSON, actual)
			sdkResp, err := sdkClient.CheckForUpdates(
				context.Background(),
				faynosync.CheckOptions{
					Owner:    decodeScenarioValue(scenario.Owner),
					AppName:  decodeScenarioValue(scenario.AppName),
					Version:  decodeScenarioValue(scenario.Version),
					Channel:  decodeScenarioValue(scenario.ChannelName),
					Platform: decodeScenarioValue(scenario.Platform),
					Arch:     decodeScenarioValue(scenario.Arch),
					DeviceID: decodeScenarioValue(scenario.DeviceID),
					DownloadToken: scenarioDownloadToken(t, decodeScenarioValue(scenario.AppName),
						decodeScenarioValue(scenario.ChannelName)),
				},
			)
			require.NoError(t, err)
			assertSDKMatchesScenario(t, scenario.ExpectedJSON, sdkResp)
		})
	}
}

func TestTelemetryWithVariousParams(t *testing.T) {
	if !viper.GetBool("ENABLE_TELEMETRY") || redisClient == nil {
		t.Skip("telemetry integration tests require ENABLE_TELEMETRY=true and Redis connection")
	}

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/telemetry", func(c *gin.Context) {
		handler.GetTelemetry(c)
	})
	endDate := time.Now().UTC().Truncate(24 * time.Hour)
	startDate := endDate.AddDate(0, 0, -7)
	dateRange, dailyStats := generateDateRangeAndStats(startDate, 8)
	scenarios := []struct {
		Name         string
		QueryParams  string
		ExpectedCode int
		ExpectedJSON map[string]interface{}
	}{
		{
			Name:         "Nightly universalPlatform + secondArch",
			QueryParams:  "range=week&apps=testapp&channels=nightly&platforms=universalPlatform&architectures=secondArch",
			ExpectedCode: http.StatusOK,
			ExpectedJSON: map[string]interface{}{
				"date":       startDate.Format("2006-01-02"),
				"date_range": dateRange,
				"admin":      "admin",
				"summary": map[string]interface{}{
					"total_requests":               float64(10),
					"unique_clients":               float64(1),
					"clients_using_latest_version": float64(0),
					"clients_outdated":             float64(1),
					"total_active_apps":            float64(1),
				},
				"versions": map[string]interface{}{
					"used_versions_count": float64(1),
					"known_versions": []interface{}{
						"0.0.1.138", "0.0.3.138",
					},
					"usage": []interface{}{
						map[string]interface{}{
							"version":      "0.0.1.138",
							"client_count": float64(1),
						},
					},
				},
				"platforms": []interface{}{
					map[string]interface{}{
						"platform":     "universalPlatform",
						"client_count": float64(1),
					},
				},
				"architectures": []interface{}{
					map[string]interface{}{
						"arch":         "secondArch",
						"client_count": float64(1),
					},
				},
				"channels": []interface{}{
					map[string]interface{}{
						"channel":      "nightly",
						"client_count": float64(1),
					},
				},
				"daily_stats": []interface{}{
					map[string]interface{}{"date": dailyStats[0], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[1], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[2], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[3], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[4], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[5], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[6], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[7], "total_requests": float64(10), "unique_clients": float64(1), "clients_using_latest_version": float64(0), "clients_outdated": float64(1)},
				},
			},
		},

		{
			Name:         "Nightly universalPlatform + universalArch",
			QueryParams:  "range=week&apps=testapp&channels=nightly&platforms=universalPlatform&architectures=universalArch",
			ExpectedCode: http.StatusOK,
			ExpectedJSON: map[string]interface{}{
				"date":       startDate.Format("2006-01-02"),
				"date_range": dateRange,
				"admin":      "admin",
				"summary": map[string]interface{}{
					"total_requests":               float64(10),
					"unique_clients":               float64(1),
					"clients_using_latest_version": float64(0),
					"clients_outdated":             float64(1),
					"total_active_apps":            float64(1),
				},
				"versions": map[string]interface{}{
					"used_versions_count": float64(1),
					"known_versions": []interface{}{
						"0.0.1.138", "0.0.3.138",
					},
					"usage": []interface{}{
						map[string]interface{}{
							"version":      "0.0.1.138",
							"client_count": float64(1),
						},
					},
				},
				"platforms": []interface{}{
					map[string]interface{}{
						"platform":     "universalPlatform",
						"client_count": float64(1),
					},
				},
				"architectures": []interface{}{
					map[string]interface{}{
						"arch":         "universalArch",
						"client_count": float64(1),
					},
				},
				"channels": []interface{}{
					map[string]interface{}{
						"channel":      "nightly",
						"client_count": float64(1),
					},
				},
				"daily_stats": []interface{}{
					map[string]interface{}{"date": dailyStats[0], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[1], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[2], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[3], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[4], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[5], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[6], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[7], "total_requests": float64(10), "unique_clients": float64(1), "clients_using_latest_version": float64(0), "clients_outdated": float64(1)},
				},
			},
		},
		{
			Name:         "Stable secondPlatform + universalArch",
			QueryParams:  "range=week&apps=testapp&channels=stable&platforms=secondPlatform&architectures=universalArch",
			ExpectedCode: http.StatusOK,
			ExpectedJSON: map[string]interface{}{
				"date":       startDate.Format("2006-01-02"),
				"date_range": dateRange,
				"admin":      "admin",
				"summary": map[string]interface{}{
					"total_requests":               float64(10),
					"unique_clients":               float64(1),
					"clients_using_latest_version": float64(0),
					"clients_outdated":             float64(1),
					"total_active_apps":            float64(1),
				},
				"versions": map[string]interface{}{
					"used_versions_count": float64(1),
					"known_versions": []interface{}{
						"0.0.1.138", "0.0.3.138",
					},
					"usage": []interface{}{
						map[string]interface{}{
							"version":      "0.0.3.138",
							"client_count": float64(1),
						},
					},
				},
				"platforms": []interface{}{
					map[string]interface{}{
						"platform":     "secondPlatform",
						"client_count": float64(1),
					},
				},
				"architectures": []interface{}{
					map[string]interface{}{
						"arch":         "universalArch",
						"client_count": float64(1),
					},
				},
				"channels": []interface{}{
					map[string]interface{}{
						"channel":      "stable",
						"client_count": float64(1),
					},
				},
				"daily_stats": []interface{}{
					map[string]interface{}{"date": dailyStats[0], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[1], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[2], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[3], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[4], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[5], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[6], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[7], "total_requests": float64(10), "unique_clients": float64(1), "clients_using_latest_version": float64(0), "clients_outdated": float64(1)},
				},
			},
		},
		{
			Name:         "Newer version — stable universal",
			QueryParams:  "range=week&apps=testapp&channels=stable&platforms=universalPlatform&architectures=universalArch",
			ExpectedCode: http.StatusOK,
			ExpectedJSON: map[string]interface{}{
				"date":       startDate.Format("2006-01-02"),
				"date_range": dateRange,
				"admin":      "admin",
				"summary": map[string]interface{}{
					"total_requests":               float64(10),
					"unique_clients":               float64(1),
					"clients_using_latest_version": float64(0),
					"clients_outdated":             float64(1),
					"total_active_apps":            float64(1),
				},
				"versions": map[string]interface{}{
					"used_versions_count": float64(1),
					"known_versions": []interface{}{
						"0.0.1.138", "0.0.3.138",
					},
					"usage": []interface{}{
						map[string]interface{}{
							"version":      "0.0.3.138",
							"client_count": float64(1),
						},
					},
				},
				"platforms": []interface{}{
					map[string]interface{}{
						"platform":     "universalPlatform",
						"client_count": float64(1),
					},
				},
				"architectures": []interface{}{
					map[string]interface{}{
						"arch":         "universalArch",
						"client_count": float64(1),
					},
				},
				"channels": []interface{}{
					map[string]interface{}{
						"channel":      "stable",
						"client_count": float64(1),
					},
				},
				"daily_stats": []interface{}{
					map[string]interface{}{"date": dailyStats[0], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[1], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[2], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[3], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[4], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[5], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[6], "total_requests": float64(0), "unique_clients": float64(0), "clients_using_latest_version": float64(0), "clients_outdated": float64(0)},
					map[string]interface{}{"date": dailyStats[7], "total_requests": float64(10), "unique_clients": float64(1), "clients_using_latest_version": float64(0), "clients_outdated": float64(1)},
				},
			},
		},
	}

	for _, s := range scenarios {
		t.Run(s.Name, func(t *testing.T) {
			w := httptest.NewRecorder()
			req, err := http.NewRequest("GET", "/telemetry?"+s.QueryParams, nil)
			if err != nil {
				t.Fatal(err)
			}

			req.Header.Set("Authorization", "Bearer "+authToken)
			router.ServeHTTP(w, req)

			assert.Equal(t, s.ExpectedCode, w.Code)

			var actual map[string]interface{}
			err = json.Unmarshal(w.Body.Bytes(), &actual)
			if err != nil {
				t.Fatalf("failed to parse response JSON: %v\nBody: %s", err, w.Body.String())
			}

			assert.Equal(t, s.ExpectedJSON, actual)
		})
	}

	t.Run("Deduplicates repeated client IDs across week and month ranges", func(t *testing.T) {
		if !viper.GetBool("ENABLE_TELEMETRY") || redisClient == nil {
			t.Skip("redis telemetry backend is not configured")
		}
		ctx := context.Background()
		dedupApp := fmt.Sprintf("testapp-dedup-%d", time.Now().UTC().UnixNano())
		dedupClientID := "dedup-client-001"
		dedupVersion := "9.9.9"
		dateA := startDate.AddDate(0, 0, 1).Format("2006-01-02")
		dateB := startDate.AddDate(0, 0, 2).Format("2006-01-02")

		baseKey := fmt.Sprintf("stats:admin:%s", dedupApp)
		seededKeys := []string{
			fmt.Sprintf("%s:requests:%s", baseKey, dateA),
			fmt.Sprintf("%s:requests:%s", baseKey, dateB),
			fmt.Sprintf("%s:unique_clients:%s", baseKey, dateA),
			fmt.Sprintf("%s:unique_clients:%s", baseKey, dateB),
			fmt.Sprintf("%s:clients_using_latest_version:%s", baseKey, dateA),
			fmt.Sprintf("%s:clients_using_latest_version:%s", baseKey, dateB),
			fmt.Sprintf("%s:clients_outdated:%s", baseKey, dateA),
			fmt.Sprintf("%s:clients_outdated:%s", baseKey, dateB),
			fmt.Sprintf("%s:version_usage:%s:%s", baseKey, dateA, dedupVersion),
			fmt.Sprintf("%s:version_usage:%s:%s", baseKey, dateB, dedupVersion),
			fmt.Sprintf("%s:known_versions", baseKey),
		}

		t.Cleanup(func() {
			if len(seededKeys) > 0 {
				_ = redisClient.Del(ctx, seededKeys...).Err()
			}
		})

		assert.NoError(t, redisClient.Set(ctx, seededKeys[0], 3, 0).Err())
		assert.NoError(t, redisClient.Set(ctx, seededKeys[1], 2, 0).Err())
		assert.NoError(t, redisClient.SAdd(ctx, seededKeys[2], dedupClientID).Err())
		assert.NoError(t, redisClient.SAdd(ctx, seededKeys[3], dedupClientID).Err())
		assert.NoError(t, redisClient.SAdd(ctx, seededKeys[4], dedupClientID).Err())
		assert.NoError(t, redisClient.SAdd(ctx, seededKeys[5], dedupClientID).Err())
		assert.NoError(t, redisClient.SAdd(ctx, seededKeys[6], dedupClientID).Err())
		assert.NoError(t, redisClient.SAdd(ctx, seededKeys[7], dedupClientID).Err())
		assert.NoError(t, redisClient.SAdd(ctx, seededKeys[8], dedupClientID).Err())
		assert.NoError(t, redisClient.SAdd(ctx, seededKeys[9], dedupClientID).Err())
		assert.NoError(t, redisClient.SAdd(ctx, seededKeys[10], dedupVersion).Err())

		queryScenarios := []string{
			fmt.Sprintf("range=week&apps=%s", dedupApp),
			fmt.Sprintf("range=month&apps=%s", dedupApp),
		}

		for _, query := range queryScenarios {
			w := httptest.NewRecorder()
			req, err := http.NewRequest("GET", "/telemetry?"+query, nil)
			if err != nil {
				t.Fatal(err)
			}
			req.Header.Set("Authorization", "Bearer "+authToken)
			router.ServeHTTP(w, req)

			assert.Equal(t, http.StatusOK, w.Code)

			var actual map[string]interface{}
			err = json.Unmarshal(w.Body.Bytes(), &actual)
			if err != nil {
				t.Fatalf("failed to parse response JSON: %v\nBody: %s", err, w.Body.String())
			}

			summary, ok := actual["summary"].(map[string]interface{})
			if !ok {
				t.Fatalf("summary is missing or invalid: %v", actual["summary"])
			}

			assert.Equal(t, float64(5), summary["total_requests"])
			assert.Equal(t, float64(1), summary["unique_clients"])
			assert.Equal(t, float64(1), summary["clients_using_latest_version"])
			assert.Equal(t, float64(1), summary["clients_outdated"])
			assert.Equal(t, float64(1), summary["total_active_apps"])

			versions, ok := actual["versions"].(map[string]interface{})
			if !ok {
				t.Fatalf("versions is missing or invalid: %v", actual["versions"])
			}

			assert.Equal(t, float64(1), versions["used_versions_count"])
			usage, ok := versions["usage"].([]interface{})
			if !ok || len(usage) != 1 {
				t.Fatalf("versions.usage is invalid: %v", versions["usage"])
			}
			usageEntry, ok := usage[0].(map[string]interface{})
			if !ok {
				t.Fatalf("versions.usage[0] is invalid: %v", usage[0])
			}
			assert.Equal(t, dedupVersion, usageEntry["version"])
			assert.Equal(t, float64(1), usageEntry["client_count"])
		}
	})
}

func TestTelemetryLuaEmitsArchAndChannelKeys(t *testing.T) {
	if !viper.GetBool("ENABLE_TELEMETRY") || redisClient == nil {
		t.Skip("redis telemetry backend is not configured")
	}

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	appHandler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/telemetry", func(c *gin.Context) {
		appHandler.GetTelemetry(c)
	})

	ctx := context.Background()
	admin := "admin"
	testApp := fmt.Sprintf("telemetry-raw-keys-%d", time.Now().UTC().UnixNano())
	clientID := "lua-key-client-1"
	testDate := time.Now().UTC().Format("2006-01-02")
	baseKey := fmt.Sprintf("stats:%s:%s", admin, testApp)

	keysToCleanup := []string{
		fmt.Sprintf("%s:requests:%s", baseKey, testDate),
		fmt.Sprintf("%s:unique_clients:%s", baseKey, testDate),
		fmt.Sprintf("%s:architectures:%s:arm64", baseKey, testDate),
		fmt.Sprintf("%s:channels:%s:stable", baseKey, testDate),
	}
	t.Cleanup(func() {
		_ = redisClient.Del(ctx, keysToCleanup...).Err()
	})

	assert.NoError(t, redisClient.Set(ctx, keysToCleanup[0], 1, 0).Err())
	assert.NoError(t, redisClient.SAdd(ctx, keysToCleanup[1], clientID).Err())
	assert.NoError(t, redisClient.SAdd(ctx, keysToCleanup[2], clientID).Err())
	assert.NoError(t, redisClient.SAdd(ctx, keysToCleanup[3], clientID).Err())

	w := httptest.NewRecorder()
	req, err := http.NewRequest("GET", fmt.Sprintf("/telemetry?date=%s&apps=%s", testDate, testApp), nil)
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer "+authToken)
	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)

	var result map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
		t.Fatalf("failed to parse telemetry response: %v\nBody: %s", err, w.Body.String())
	}

	architectures, ok := result["architectures"].([]interface{})
	if !ok || len(architectures) != 1 {
		t.Fatalf("unexpected architectures payload: %v", result["architectures"])
	}
	archEntry, ok := architectures[0].(map[string]interface{})
	if !ok {
		t.Fatalf("unexpected architectures[0] payload: %v", architectures[0])
	}
	assert.Equal(t, "arm64", archEntry["arch"])
	_, hasArchPlatformKey := archEntry["platform"]
	assert.False(t, hasArchPlatformKey)

	channels, ok := result["channels"].([]interface{})
	if !ok || len(channels) != 1 {
		t.Fatalf("unexpected channels payload: %v", result["channels"])
	}
	channelEntry, ok := channels[0].(map[string]interface{})
	if !ok {
		t.Fatalf("unexpected channels[0] payload: %v", channels[0])
	}
	assert.Equal(t, "stable", channelEntry["channel"])
	_, hasChannelPlatformKey := channelEntry["platform"]
	assert.False(t, hasChannelPlatformKey)
}

func TestTelemetryBeaconUsesAllowListAndExistingRedisKeys(t *testing.T) {
	if !viper.GetBool("ENABLE_TELEMETRY") || redisClient == nil {
		t.Skip("redis telemetry backend is not configured")
	}

	ctx := context.Background()
	owner := fmt.Sprintf("beacon-owner-%d", time.Now().UTC().UnixNano())
	appName := "beaconapp"
	channel := "stable"
	platform := "darwin"
	arch := "arm64"
	version := "1.2.3"
	dateStr := time.Now().UTC().Format("2006-01-02")
	baseKey := fmt.Sprintf("stats:%s:%s", owner, appName)
	keysToCleanup := []string{
		fmt.Sprintf("%s:requests:%s", baseKey, dateStr),
		fmt.Sprintf("%s:unique_clients:%s", baseKey, dateStr),
		fmt.Sprintf("%s:channels:%s:%s", baseKey, dateStr, channel),
		fmt.Sprintf("%s:platforms:%s:%s", baseKey, dateStr, platform),
		fmt.Sprintf("%s:architectures:%s:%s", baseKey, dateStr, arch),
		fmt.Sprintf("%s:known_versions", baseKey),
		fmt.Sprintf("%s:version_usage:%s:%s", baseKey, dateStr, version),
		fmt.Sprintf("%s:clients_using_latest_version:%s", baseKey, dateStr),
		fmt.Sprintf("%s:clients_outdated:%s", baseKey, dateStr),
	}

	metaCollection := mongoDatabase.Collection("apps_meta")
	appsCollection := mongoDatabase.Collection("apps")
	t.Cleanup(func() {
		_, _ = metaCollection.DeleteMany(ctx, bson.M{"owner": owner})
		_, _ = appsCollection.DeleteMany(ctx, bson.M{"owner": owner})
		_ = redisClient.Del(ctx, keysToCleanup...).Err()
	})
	_ = redisClient.Del(ctx, keysToCleanup...).Err()
	_, _ = metaCollection.DeleteMany(ctx, bson.M{"owner": owner})
	_, _ = appsCollection.DeleteMany(ctx, bson.M{"owner": owner})

	appID := primitive.NewObjectID()
	channelID := primitive.NewObjectID()
	platformID := primitive.NewObjectID()
	archID := primitive.NewObjectID()
	now := primitive.NewDateTimeFromTime(time.Now().UTC())

	_, err := metaCollection.InsertMany(ctx, []interface{}{
		bson.M{"_id": appID, "app_name": appName, "owner": owner, "updated_at": now},
		bson.M{"_id": channelID, "channel_name": channel, "owner": owner, "updated_at": now},
		bson.M{"_id": platformID, "platform_name": platform, "owner": owner, "updated_at": now},
		bson.M{"_id": archID, "arch_id": arch, "owner": owner, "updated_at": now},
	})
	require.NoError(t, err)

	_, err = appsCollection.InsertOne(ctx, bson.M{
		"_id":        primitive.NewObjectID(),
		"app_id":     appID,
		"version":    version,
		"channel_id": channelID,
		"published":  true,
		"owner":      owner,
		"artifacts": []bson.M{
			{"platform": platformID, "arch": archID, "package": ".zip", "link": "https://example.com/beacon.zip"},
		},
		"updated_at": now,
	})
	require.NoError(t, err)
	require.NoError(t, info.ReloadTelemetryAllowList(ctx, mongoDatabase))

	router := gin.Default()
	appHandler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/telemetry/beacon", func(c *gin.Context) {
		appHandler.TelemetryBeacon(c)
	})

	validQuery := fmt.Sprintf(
		"/telemetry/beacon?owner=%s&app_name=%s&version=%s&channel=%s&platform=%s&arch=%s",
		url.QueryEscape(owner),
		url.QueryEscape(appName),
		url.QueryEscape(version),
		url.QueryEscape(channel),
		url.QueryEscape(platform),
		url.QueryEscape(arch),
	)

	w := httptest.NewRecorder()
	req, err := http.NewRequest("GET", validQuery+"&is_latest=true", nil)
	require.NoError(t, err)
	req.Header.Set("X-Device-ID", "beacon-device-latest")
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusNoContent, w.Code)

	requestCount, err := redisClient.Get(ctx, keysToCleanup[0]).Int()
	require.NoError(t, err)
	assert.Equal(t, 1, requestCount)
	assert.Equal(t, int64(1), redisClient.SCard(ctx, keysToCleanup[1]).Val())
	assert.Equal(t, int64(1), redisClient.SCard(ctx, keysToCleanup[2]).Val())
	assert.Equal(t, int64(1), redisClient.SCard(ctx, keysToCleanup[3]).Val())
	assert.Equal(t, int64(1), redisClient.SCard(ctx, keysToCleanup[4]).Val())
	assert.Equal(t, int64(1), redisClient.SCard(ctx, keysToCleanup[6]).Val())
	assert.Equal(t, int64(1), redisClient.SCard(ctx, keysToCleanup[7]).Val())
	assert.Equal(t, int64(0), redisClient.SCard(ctx, keysToCleanup[8]).Val())

	w = httptest.NewRecorder()
	req, err = http.NewRequest("GET", validQuery+"&is_latest=false", nil)
	require.NoError(t, err)
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusNoContent, w.Code)
	requestCountAfterMissingDeviceID, err := redisClient.Get(ctx, keysToCleanup[0]).Int()
	require.NoError(t, err)
	assert.Equal(t, 1, requestCountAfterMissingDeviceID)

	w = httptest.NewRecorder()
	req, err = http.NewRequest("GET", validQuery, nil)
	require.NoError(t, err)
	req.Header.Set("X-Device-ID", "beacon-device-unknown-latest")
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusNoContent, w.Code)
	assert.False(t, redisClient.SIsMember(ctx, keysToCleanup[7], "beacon-device-unknown-latest").Val())
	assert.False(t, redisClient.SIsMember(ctx, keysToCleanup[8], "beacon-device-unknown-latest").Val())

	noVersionQuery := fmt.Sprintf(
		"/telemetry/beacon?owner=%s&app_name=%s&channel=%s&platform=%s&arch=%s",
		url.QueryEscape(owner),
		url.QueryEscape(appName),
		url.QueryEscape(channel),
		url.QueryEscape(platform),
		url.QueryEscape(arch),
	)

	w = httptest.NewRecorder()
	req, err = http.NewRequest("GET", noVersionQuery+"&is_latest=false", nil)
	require.NoError(t, err)
	req.Header.Set("X-Device-ID", "beacon-device-outdated-no-version")
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusNoContent, w.Code)
	assert.False(t, redisClient.SIsMember(ctx, keysToCleanup[6], "beacon-device-outdated-no-version").Val())
	assert.True(t, redisClient.SIsMember(ctx, keysToCleanup[8], "beacon-device-outdated-no-version").Val())

	w = httptest.NewRecorder()
	req, err = http.NewRequest("GET", strings.Replace(validQuery, "arch=arm64", "arch=x64", 1)+"&is_latest=false", nil)
	require.NoError(t, err)
	req.Header.Set("X-Device-ID", "beacon-device-invalid")
	router.ServeHTTP(w, req)
	assert.Equal(t, http.StatusNoContent, w.Code)

	requestCountAfterInvalid, err := redisClient.Get(ctx, keysToCleanup[0]).Int()
	require.NoError(t, err)
	assert.Equal(t, 3, requestCountAfterInvalid)
	assert.False(t, redisClient.SIsMember(ctx, keysToCleanup[8], "beacon-device-invalid").Val())
}

func TestListAppsWithSecondUser(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /app/list endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/app/list", func(c *gin.Context) {
		handler.ListApps(c)
	})

	// Create a POST request for the /app/list endpoint.
	req, err := http.NewRequest("GET", "/app/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"apps":null}`
	assert.Equal(t, expected, w.Body.String())
}
func TestListChannelsWithSecondUser(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /channel/list endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/channel/list", func(c *gin.Context) {
		handler.ListChannels(c)
	})

	// Create a POST request for the /channel/list endpoint.
	req, err := http.NewRequest("GET", "/channel/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"channels":null}`
	assert.Equal(t, expected, w.Body.String())
}

func TestListPlatformsWithSecondUser(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /platform/list endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/platform/list", func(c *gin.Context) {
		handler.ListPlatforms(c)
	})

	// Create a POST request for the /platform/list endpoint.
	req, err := http.NewRequest("GET", "/platform/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"platforms":null}`
	assert.Equal(t, expected, w.Body.String())
}

func TestListArchsWithSecondUser(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /arch/list endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.GET("/arch/list", func(c *gin.Context) {
		handler.ListArchs(c)
	})

	// Create a POST request for the /arch/list endpoint.
	req, err := http.NewRequest("GET", "/arch/list", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusOK, w.Code)

	expected := `{"archs":null}`
	assert.Equal(t, expected, w.Body.String())
}

func TestUpdateAppWithSecondUser(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /app/update route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/app/update", func(c *gin.Context) {
		handler.UpdateApp(c)
	})

	// Create multipart/form-data request body
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	// Add a field for the channel to the form
	dataPart, err := writer.CreateFormField("data")
	if err != nil {
		t.Fatal(err)
	}
	payload := fmt.Sprintf(`{"id": "%s", "app":"newApp"}`, idTestappApp)
	_, err = dataPart.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}

	// Close the writer to finalize the form data
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	// Create a POST request to the /app/update endpoint
	req, err := http.NewRequest("POST", "/app/update", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Set the Content-Type header for multipart/form-data
	req.Header.Set("Content-Type", writer.FormDataContentType())

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusNotFound {
		t.Fatalf("expected status %d; got %d", http.StatusNotFound, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"error":"app_name not found in apps_meta collection"}`
	assert.Equal(t, expected, w.Body.String())
}

func TestUpdateChannelWithSecondUser(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /channel/update route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/channel/update", func(c *gin.Context) {
		handler.UpdateChannel(c)
	})

	payload := fmt.Sprintf(`{
		"id": "%s",
		"channel": "unstable"
	}`, idStableChannel)
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /channel/update endpoint
	req, err := http.NewRequest("POST", "/channel/update", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 500 Internal Server Error)
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("expected status %d; got %d", http.StatusInternalServerError, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"error":"you don't have permission to update this channel"}`
	assert.Equal(t, expected, w.Body.String())
}

func TestUpdatePlatformWithSecondUser(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /platform/update route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/platform/update", func(c *gin.Context) {
		handler.UpdatePlatform(c)
	})

	payload := fmt.Sprintf(`{
		"id": "%s",
		"platform": "newPlatform",
		"updaters": [
			{ "type": "manual", "default": true }
		]
	}`, platformId)
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /platform/update endpoint
	req, err := http.NewRequest("POST", "/platform/update", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("expected status %d; got %d", http.StatusInternalServerError, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"error":"you don't have permission to update this platform"}`
	assert.Equal(t, expected, w.Body.String())
}

func TestUpdateArchWithSecondUser(t *testing.T) {
	// Initialize Gin router and recorder for the test
	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the handler for the /arch/update route
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.POST("/arch/update", func(c *gin.Context) {
		handler.UpdateArch(c)
	})

	payload := fmt.Sprintf(`{
		"id": "%s",
		"arch": "newArch"
	}`, archId)
	body := bytes.NewReader([]byte(payload))

	// Create a POST request to the /arch/update endpoint
	req, err := http.NewRequest("POST", "/arch/update", body)
	if err != nil {
		t.Fatal(err)
	}
	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Set the Content-Type header for JSON
	req.Header.Set("Content-Type", "application/json")

	// Serve the request using the Gin router
	router.ServeHTTP(w, req)
	logrus.Infoln("Response Body:", w.Body.String())
	// Check the response status code (expecting 200 OK)
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("expected status %d; got %d", http.StatusInternalServerError, w.Code)
	}

	// Parse the JSON response
	var response map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &response)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"error":"you don't have permission to update this arch"}`
	assert.Equal(t, expected, w.Body.String())
}

func TestMultipleDeleteWithSameExtensionArtifactsAndDiffPlatformsArchsWithSecondUser(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))

	// Define the route for the /apps/delete endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.DELETE("/apps/delete", func(c *gin.Context) {
		handler.DeleteSpecificVersionOfApp(c)
	})

	// Iterate over the uploadedAppIDsWithSameExtension and send a DELETE request for each ID.
	for _, appID := range uploadedAppIDsWithSameExtension {
		w := httptest.NewRecorder()

		req, err := http.NewRequest("DELETE", "/apps/delete?id="+appID, nil)
		if err != nil {
			t.Fatal(err)
		}

		// Set the Authorization header.
		req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
		// Serve the request using the Gin router.
		router.ServeHTTP(w, req)

		// Check the response status code for each request.
		assert.Equal(t, http.StatusInternalServerError, w.Code)

		expected := `{"details":"you don't have permission to delete this item","error":"failed to delete specific version of app"}`
		assert.Equal(t, expected, w.Body.String())
	}
}

func TestDeleteNightlyChannelWithSecondUser(t *testing.T) {

	router := gin.Default()
	router.Use(utils.AuthMiddleware(mongoDatabase))
	w := httptest.NewRecorder()

	// Define the route for the /channel/delete endpoint.
	handler := handler.NewAppHandler(client, appDB, mongoDatabase, redisClient, viper.GetBool("PERFORMANCE_MODE"))
	router.DELETE("/channel/delete", func(c *gin.Context) {
		handler.DeleteChannel(c)
	})

	// Create a DELETE request for the /channel/delete endpoint.
	req, err := http.NewRequest("DELETE", "/channel/delete?id="+idNightlyChannel, nil)
	if err != nil {
		t.Fatal(err)
	}

	// Set the Authorization header.
	req.Header.Set("Authorization", "Bearer "+authTokenSecondUser)
	// Serve the request using the Gin router.
	router.ServeHTTP(w, req)

	// Check the response status code.
	assert.Equal(t, http.StatusInternalServerError, w.Code)

	expected := `{"details":"you don't have permission to delete this channel","error":"failed to delete channel"}`
	assert.Equal(t, expected, w.Body.String())
}

func TestDeletePlatformWithSecondUser(t *