🏡 index : ironmagma/quintodrome.git

package server

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/navidrome/navidrome/conf/configtest"
	"github.com/navidrome/navidrome/tests"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("createUnixSocketFile", func() {
	var socketPath string

	BeforeEach(func() {
		tempDir, _ := os.MkdirTemp("", "create_unix_socket_file_test")
		socketPath = filepath.Join(tempDir, "test.sock")
		DeferCleanup(func() {
			_ = os.RemoveAll(tempDir)
		})
	})

	When("unixSocketPerm is valid", func() {
		It("updates the permission of the unix socket file and returns nil", func() {
			tests.SkipOnWindows("uses Unix file permission bits")
			_, err := createUnixSocketFile(socketPath, "0777")
			fileInfo, _ := os.Stat(socketPath)
			actualPermission := fileInfo.Mode().Perm()

			Expect(actualPermission).To(Equal(os.FileMode(0777)))
			Expect(err).ToNot(HaveOccurred())

		})
	})

	When("unixSocketPerm is invalid", func() {
		It("returns an error", func() {
			_, err := createUnixSocketFile(socketPath, "invalid")
			Expect(err).To(HaveOccurred())

		})
	})

	When("file already exists", func() {
		It("recreates the file as a socket with the right permissions", func() {
			tests.SkipOnWindows("uses Unix file permission bits")
			_, err := os.Create(socketPath)
			Expect(err).ToNot(HaveOccurred())
			Expect(os.Chmod(socketPath, os.FileMode(0777))).To(Succeed())

			_, err = createUnixSocketFile(socketPath, "0600")
			Expect(err).ToNot(HaveOccurred())
			fileInfo, _ := os.Stat(socketPath)
			Expect(fileInfo.Mode().Perm()).To(Equal(os.FileMode(0600)))
			Expect(fileInfo.Mode().Type()).To(Equal(fs.ModeSocket))
		})
	})
})

var _ = Describe("TLS support", func() {
	Describe("validateTLSCertificates", func() {
		const testDataDir = "server/testdata"

		When("certificate and key are valid and unencrypted", func() {
			It("returns nil", func() {
				certFile := filepath.Join(testDataDir, "test_cert.pem")
				keyFile := filepath.Join(testDataDir, "test_key.pem")
				err := validateTLSCertificates(certFile, keyFile)
				Expect(err).ToNot(HaveOccurred())
			})
		})

		When("private key is encrypted with PKCS#8 format", func() {
			It("returns an error with helpful message", func() {
				certFile := filepath.Join(testDataDir, "test_cert_encrypted.pem")
				keyFile := filepath.Join(testDataDir, "test_key_encrypted.pem")
				err := validateTLSCertificates(certFile, keyFile)
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("encrypted"))
				Expect(err.Error()).To(ContainSubstring("openssl"))
			})
		})

		When("private key is encrypted with legacy format (Proc-Type header)", func() {
			It("returns an error with helpful message", func() {
				certFile := filepath.Join(testDataDir, "test_cert.pem")
				keyFile := filepath.Join(testDataDir, "test_key_encrypted_legacy.pem")
				err := validateTLSCertificates(certFile, keyFile)
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("encrypted"))
				Expect(err.Error()).To(ContainSubstring("openssl"))
			})
		})

		When("key file does not exist", func() {
			It("returns an error", func() {
				certFile := filepath.Join(testDataDir, "test_cert.pem")
				keyFile := filepath.Join(testDataDir, "nonexistent.pem")
				err := validateTLSCertificates(certFile, keyFile)
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("reading TLS key file"))
			})
		})

		When("key file does not contain valid PEM", func() {
			It("returns an error", func() {
				// Create a temp file with invalid PEM content
				tmpFile, err := os.CreateTemp("", "invalid_key*.pem")
				Expect(err).ToNot(HaveOccurred())
				DeferCleanup(func() {
					_ = os.Remove(tmpFile.Name())
				})
				_, err = tmpFile.WriteString("not a valid PEM file")
				Expect(err).ToNot(HaveOccurred())
				_ = tmpFile.Close()

				certFile := filepath.Join(testDataDir, "test_cert.pem")
				err = validateTLSCertificates(certFile, tmpFile.Name())
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("valid PEM block"))
			})
		})

		When("certificate file does not exist", func() {
			It("returns an error from tls.LoadX509KeyPair", func() {
				certFile := filepath.Join(testDataDir, "nonexistent_cert.pem")
				keyFile := filepath.Join(testDataDir, "test_key.pem")
				err := validateTLSCertificates(certFile, keyFile)
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("loading TLS certificate/key pair"))
			})
		})
	})

	Describe("Server TLS", func() {
		const testDataDir = "server/testdata"

		When("server is started with valid TLS certificates", func() {
			It("accepts HTTPS connections", func() {
				DeferCleanup(configtest.SetupConfig())

				// Create server with mock dependencies
				ds := &tests.MockDataStore{}
				server := New(ds, nil, nil)

				// Load the test certificate to create a trusted CA pool
				certFile := filepath.Join(testDataDir, "test_cert.pem")
				keyFile := filepath.Join(testDataDir, "test_key.pem")
				caCert, err := os.ReadFile(certFile)
				Expect(err).ToNot(HaveOccurred())

				caCertPool := x509.NewCertPool()
				caCertPool.AppendCertsFromPEM(caCert)

				// Create an HTTPS client that trusts our test certificate
				httpClient := &http.Client{
					Timeout: 5 * time.Second,
					Transport: &http.Transport{
						TLSClientConfig: &tls.Config{
							RootCAs:    caCertPool,
							MinVersion: tls.VersionTLS12,
						},
					},
				}

				// Start the server in a goroutine
				ctx, cancel := context.WithCancel(GinkgoT().Context())
				defer cancel()

				errChan := make(chan error, 1)
				go func() {
					errChan <- server.Run(ctx, "127.0.0.1", 14534, certFile, keyFile)
				}()

				Eventually(func() error {
					// Make an HTTPS request to the server
					resp, err := httpClient.Get("https://127.0.0.1:14534/ping")
					if err != nil {
						return err
					}
					defer resp.Body.Close()
					if resp.StatusCode != http.StatusOK {
						return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
					}
					return nil
				}, 2*time.Second, 100*time.Millisecond).Should(Succeed())

				// Stop the server
				cancel()

				// Wait for server to stop (with timeout)
				select {
				case <-errChan:
					// Server stopped
				case <-time.After(2 * time.Second):
					Fail("Server did not stop in time")
				}
			})
		})
	})
})