package auth_test import ( "crypto/tls" "crypto/x509" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/J3vb/OwnCord/Server/auth" "github.com/J3vb/OwnCord/Server/config" ) func TestGenerateSelfSignedCreatesFiles(t *testing.T) { tmpDir := t.TempDir() certFile := filepath.Join(tmpDir, "cert.pem") keyFile := filepath.Join(tmpDir, "key.pem") if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil { t.Fatalf("GenerateSelfSigned() error: %v", err) } if _, err := os.Stat(certFile); os.IsNotExist(err) { t.Error("cert.pem not created") } if _, err := os.Stat(keyFile); os.IsNotExist(err) { t.Error("key.pem not created") } } func TestGenerateSelfSignedProducesValidCert(t *testing.T) { tmpDir := t.TempDir() certFile := filepath.Join(tmpDir, "cert.pem") keyFile := filepath.Join(tmpDir, "key.pem") if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil { t.Fatalf("GenerateSelfSigned() error: %v", err) } // Load the generated cert/key pair. cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { t.Fatalf("tls.LoadX509KeyPair error: %v", err) } // Parse the leaf certificate. leaf, err := x509.ParseCertificate(cert.Certificate[0]) if err != nil { t.Fatalf("x509.ParseCertificate error: %v", err) } // Verify validity period is ~2 years (not the old 10y). minExpiry := time.Now().Add(1 * 365 * 24 * time.Hour) maxExpiry := time.Now().Add(3 * 365 * 24 * time.Hour) if leaf.NotAfter.Before(minExpiry) { t.Errorf("cert expires %v, expected at least 1 year from now (%v)", leaf.NotAfter, minExpiry) } if leaf.NotAfter.After(maxExpiry) { t.Errorf("cert expires %v, expected at most 3 years from now (%v)", leaf.NotAfter, maxExpiry) } // BUG-138: Leaf cert must NOT be a CA — prevents signing other certs on key compromise. if leaf.IsCA { t.Error("expected IsCA = false for self-signed leaf cert") } if leaf.KeyUsage&x509.KeyUsageCertSign != 0 { t.Error("leaf cert should not have KeyUsageCertSign") } } func TestGenerateSelfSignedInvalidCertPath(t *testing.T) { err := auth.GenerateSelfSigned("/nonexistent/dir/cert.pem", "/nonexistent/dir/key.pem") if err == nil { t.Error("GenerateSelfSigned() should error for invalid cert path") } } func TestGenerateSelfSignedInvalidKeyPath(t *testing.T) { tmpDir := t.TempDir() certFile := filepath.Join(tmpDir, "cert.pem") // Key path in non-existent dir. err := auth.GenerateSelfSigned(certFile, "/nonexistent/dir/key.pem") if err == nil { t.Error("GenerateSelfSigned() should error for invalid key path") } } func TestLoadOrGenerateSelfSigned(t *testing.T) { tmpDir := t.TempDir() certFile := filepath.Join(tmpDir, "cert.pem") keyFile := filepath.Join(tmpDir, "key.pem") cfg := config.TLSConfig{ Mode: "self_signed", CertFile: certFile, KeyFile: keyFile, } result, err := auth.LoadOrGenerate(cfg) if err != nil { t.Fatalf("LoadOrGenerate() error: %v", err) } if result.TLSConfig == nil { t.Fatal("LoadOrGenerate() returned nil TLSConfig") } if len(result.TLSConfig.Certificates) == 0 { t.Error("LoadOrGenerate() returned TLSConfig with no certificates") } if result.HTTPHandler != nil { t.Error("self_signed mode should not set HTTPHandler") } } func TestLoadOrGenerateLoadsExistingCert(t *testing.T) { tmpDir := t.TempDir() certFile := filepath.Join(tmpDir, "cert.pem") keyFile := filepath.Join(tmpDir, "key.pem") // Generate a cert first. if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil { t.Fatalf("GenerateSelfSigned() error: %v", err) } cfg := config.TLSConfig{ Mode: "self_signed", CertFile: certFile, KeyFile: keyFile, } // Load the existing cert (should not regenerate). result, err := auth.LoadOrGenerate(cfg) if err != nil { t.Fatalf("LoadOrGenerate() error: %v", err) } if len(result.TLSConfig.Certificates) == 0 { t.Error("LoadOrGenerate() returned no certificates") } } func TestLoadOrGenerateModeOff(t *testing.T) { cfg := config.TLSConfig{Mode: "off"} result, err := auth.LoadOrGenerate(cfg) if err != nil { t.Fatalf("LoadOrGenerate(mode=off) error: %v", err) } if result.TLSConfig != nil { t.Error("LoadOrGenerate(mode=off) should return nil TLSConfig") } } func TestLoadOrGenerateModeManualMissingFiles(t *testing.T) { cfg := config.TLSConfig{ Mode: "manual", CertFile: "/nonexistent/cert.pem", KeyFile: "/nonexistent/key.pem", } _, err := auth.LoadOrGenerate(cfg) if err == nil { t.Error("LoadOrGenerate(mode=manual) should error when cert/key don't exist") } } func TestLoadOrGenerateModeManualValidFiles(t *testing.T) { tmpDir := t.TempDir() certFile := filepath.Join(tmpDir, "cert.pem") keyFile := filepath.Join(tmpDir, "key.pem") // Pre-generate cert files. if err := auth.GenerateSelfSigned(certFile, keyFile); err != nil { t.Fatalf("GenerateSelfSigned() error: %v", err) } cfg := config.TLSConfig{ Mode: "manual", CertFile: certFile, KeyFile: keyFile, } result, err := auth.LoadOrGenerate(cfg) if err != nil { t.Fatalf("LoadOrGenerate(mode=manual) error: %v", err) } if len(result.TLSConfig.Certificates) == 0 { t.Error("LoadOrGenerate(mode=manual) returned no certificates") } } func TestLoadOrGenerateUnknownMode(t *testing.T) { cfg := config.TLSConfig{Mode: "unknown_mode"} _, err := auth.LoadOrGenerate(cfg) if err == nil { t.Error("LoadOrGenerate() should error for unknown TLS mode") } } // ── ACME mode tests ─────────────────────────────────────────────────────── func TestLoadOrGenerateACME_MissingDomain(t *testing.T) { cfg := config.TLSConfig{Mode: "acme", Domain: ""} _, err := auth.LoadOrGenerate(cfg) if err == nil { t.Fatal("expected error for ACME mode without domain") } if !strings.Contains(err.Error(), "domain") { t.Errorf("error should mention domain, got: %v", err) } } func TestLoadOrGenerateACME_IPAddress(t *testing.T) { cfg := config.TLSConfig{Mode: "acme", Domain: "192.168.1.1"} _, err := auth.LoadOrGenerate(cfg) if err == nil { t.Fatal("expected error for ACME mode with IP address") } if !strings.Contains(err.Error(), "IP address") { t.Errorf("error should mention IP address, got: %v", err) } } func TestLoadOrGenerateACME_WildcardDomain(t *testing.T) { cfg := config.TLSConfig{Mode: "acme", Domain: "*.example.com"} _, err := auth.LoadOrGenerate(cfg) if err == nil { t.Fatal("expected error for ACME mode with wildcard domain") } if !strings.Contains(err.Error(), "wildcard") { t.Errorf("error should mention wildcard, got: %v", err) } } func TestLoadOrGenerateACME_ValidDomain(t *testing.T) { tmpDir := t.TempDir() cacheDir := filepath.Join(tmpDir, "acme_certs") cfg := config.TLSConfig{ Mode: "acme", Domain: "chat.example.com", AcmeCacheDir: cacheDir, } result, err := auth.LoadOrGenerate(cfg) if err != nil { t.Fatalf("LoadOrGenerate(acme) error: %v", err) } if result.TLSConfig == nil { t.Fatal("ACME mode should return non-nil TLSConfig") } if result.TLSConfig.GetCertificate == nil { t.Error("ACME TLSConfig should have GetCertificate set") } if result.HTTPHandler == nil { t.Error("ACME mode should return non-nil HTTPHandler") } // Verify cache directory was created. if _, err := os.Stat(cacheDir); os.IsNotExist(err) { t.Error("ACME cache directory was not created") } } func TestLoadOrGenerateACME_HTTPRedirect(t *testing.T) { tmpDir := t.TempDir() cfg := config.TLSConfig{ Mode: "acme", Domain: "chat.example.com", AcmeCacheDir: filepath.Join(tmpDir, "acme_certs"), } result, err := auth.LoadOrGenerate(cfg) if err != nil { t.Fatalf("LoadOrGenerate(acme) error: %v", err) } // Non-challenge requests should redirect to HTTPS. req := httptest.NewRequest(http.MethodGet, "http://chat.example.com/some/path", nil) rec := httptest.NewRecorder() result.HTTPHandler.ServeHTTP(rec, req) if rec.Code != http.StatusMovedPermanently { t.Errorf("expected 301 redirect, got %d", rec.Code) } loc := rec.Header().Get("Location") if !strings.HasPrefix(loc, "https://chat.example.com/") { t.Errorf("redirect should point to HTTPS, got: %s", loc) } }