217 lines
11 KiB
Go
217 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestGenerateRoots(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
outputFile := filepath.Join(tmpDir, "test-roots.pem")
|
|
|
|
// Create a test HTTP server that returns mock root certificates
|
|
mockResponse := CTLogRootsResponse{
|
|
Certificates: []string{
|
|
// Real certificate (base64 encoded DER from OpenSSL)
|
|
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgMwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
|
// Second certificate (same but different serial for testing)
|
|
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgQwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
|
},
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.HasSuffix(r.URL.Path, "/ct/v1/get-roots") {
|
|
t.Errorf("Expected path to end with /ct/v1/get-roots, got %s", r.URL.Path)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(mockResponse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Test with default arguments
|
|
args := []string{"--source", server.URL, "--output", outputFile}
|
|
generateRoots(args, false, true, false)
|
|
|
|
// Verify output file was created
|
|
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
|
t.Fatal("Output file was not created")
|
|
}
|
|
|
|
// Verify file contents
|
|
content, err := os.ReadFile(outputFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
contentStr := string(content)
|
|
|
|
// Should contain PEM headers
|
|
if !strings.Contains(contentStr, "-----BEGIN CERTIFICATE-----") {
|
|
t.Error("Expected PEM certificate headers")
|
|
}
|
|
if !strings.Contains(contentStr, "-----END CERTIFICATE-----") {
|
|
t.Error("Expected PEM certificate footers")
|
|
}
|
|
|
|
// Count certificates (should have 2)
|
|
certCount := strings.Count(contentStr, "-----BEGIN CERTIFICATE-----")
|
|
if certCount != 2 {
|
|
t.Errorf("Expected 2 certificates, found %d", certCount)
|
|
}
|
|
}
|
|
|
|
func TestGenerateRootsWithNegativeSerial(t *testing.T) {
|
|
// Should call log.Fatalf due to invalid base64
|
|
// Can't easily test this without subprocess, so we'll skip it
|
|
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
|
}
|
|
|
|
func TestGenerateRootsHTTPError(t *testing.T) {
|
|
// Should call log.Fatalf due to HTTP error
|
|
// Can't easily test this without subprocess, so we'll skip it
|
|
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
|
}
|
|
|
|
func TestGenerateRootsInvalidJSON(t *testing.T) {
|
|
// Should call log.Fatalf due to JSON parse error
|
|
// Can't easily test this without subprocess, so we'll skip it
|
|
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
|
}
|
|
|
|
func TestGenerateRootsArgumentParsing(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
customOutputFile := filepath.Join(tmpDir, "custom-roots.pem")
|
|
|
|
mockResponse := CTLogRootsResponse{
|
|
Certificates: []string{
|
|
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgMwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
|
},
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(mockResponse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Test custom source and output
|
|
args := []string{"--source", server.URL, "--output", customOutputFile}
|
|
generateRoots(args, false, true, false)
|
|
|
|
// Verify custom output file was created
|
|
if _, err := os.Stat(customOutputFile); os.IsNotExist(err) {
|
|
t.Error("Custom output file was not created")
|
|
}
|
|
}
|
|
|
|
func TestGenerateRootsMissingSourceArgument(t *testing.T) {
|
|
// Should call log.Fatal due to missing source URL argument
|
|
// Can't easily test this without subprocess, so we'll skip it
|
|
t.Skip("Cannot easily test log.Fatal without subprocess")
|
|
}
|
|
|
|
func TestGenerateRootsMissingOutputArgument(t *testing.T) {
|
|
// Should call log.Fatal due to missing output filename argument
|
|
// Can't easily test this without subprocess, so we'll skip it
|
|
t.Skip("Cannot easily test log.Fatal without subprocess")
|
|
}
|
|
|
|
func TestGenerateRootsUnknownArgument(t *testing.T) {
|
|
// Should call log.Fatalf due to unknown argument
|
|
// Can't easily test this without subprocess, so we'll skip it
|
|
t.Skip("Cannot easily test log.Fatalf without subprocess")
|
|
}
|
|
|
|
func TestGenerateRootsDefaultArguments(t *testing.T) {
|
|
// Test that default arguments are used correctly
|
|
// This test would normally make a real HTTP request, so we'll skip it
|
|
// unless we're doing integration tests
|
|
t.Skip("Skipping test that would make real HTTP request")
|
|
}
|
|
|
|
func TestGenerateRootsSourceURLFormatting(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
outputFile := filepath.Join(tmpDir, "test-roots.pem")
|
|
|
|
mockResponse := CTLogRootsResponse{
|
|
Certificates: []string{
|
|
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgMwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
|
},
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Verify the path formatting
|
|
expectedPath := "/ct/v1/get-roots"
|
|
if r.URL.Path != expectedPath {
|
|
t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(mockResponse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Test with URL that doesn't end with /
|
|
sourceURL := strings.TrimSuffix(server.URL, "/")
|
|
args := []string{"--source", sourceURL, "--output", outputFile}
|
|
generateRoots(args, false, true, false)
|
|
|
|
// Should still work (the function adds trailing slash)
|
|
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
|
t.Error("Output file was not created when source URL lacks trailing slash")
|
|
}
|
|
}
|
|
|
|
func TestPEMEncoding(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
outputFile := filepath.Join(tmpDir, "test-roots.pem")
|
|
|
|
mockResponse := CTLogRootsResponse{
|
|
Certificates: []string{
|
|
"MIIDFzCCAf+gAwIBAgIUbOZ5dIVBuGei8T0VdIKjCpACVgMwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTA4MjgxOTA4MjlaFw0yNjA4MjgxOTA4MjlaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEmF+JcVMD9u0efdi8AkIH4xiKb/8lJveOYu5UKJ/hE7j+QN0HcK+3d0Fh83Hgny7ZgF3A8JlTDnlZrkI1+n8uxLAbvZ/PVNS286FwlnWppnarSBwFMuHp+SoGFuNIQtB5LaxVt943Oxaw4vVaFBqi0iYPFKXj2JxLIwiwKxLiZ2hS15g/JIhRXSycHjMCvukPj1wtRo/cHF83Miydw+ngFqrfXcEzvG/Yx5qvAgowvG/FEKha5zB36ywy35SHq8gX7DHs77eQ52lxd4yvYfPhIwaAzTvnpyACcvfgP/XiETMxsaPoZ3WFzK4y3Smg6zUMLAB1YE4RxUd+4NA+NBmbAgMBAAGjUzBRMB0GA1UdDgQWBBQxgJBg/4o2f1e58aBMOlyEO0svIDAfBgNVHSMEGDAWgBQxgJBg/4o2f1e58aBMOlyEO0svIDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA1Z1r8pJeYizgo30kA+Q4SICq70jvAO6XPrin2hp8MlvE7nUG2afInzNh6FXjqMI0TWctqV+wF9nnCkPBPu4ybCbm7reiJ1SojPmp/qxv+qoyXXFZxX/ugOhhJv4BbSoHxepdSPBtqQb5uZPL/1q9vYmLfsEdEdh1dI4vEbrvvZR/fV/5+ZjL2uOuWf6zw0BpMqXyC0wcoZIWKHl62yLOhdJwbUVLfKiHuwrhsdIIc0QDI74U6x5pu/eIa1hCPX9e/X8vEK8/0EBV2xGhAeeteu4bY04AjrSs0tESQO5EfACmhQM/1ytlqMx3qsPGb7pIptUttHREvr+RL6qKCRZCK",
|
|
},
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(mockResponse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
args := []string{"--source", server.URL, "--output", outputFile}
|
|
generateRoots(args, false, true, false)
|
|
|
|
// Read and verify PEM structure
|
|
content, err := os.ReadFile(outputFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Parse PEM blocks
|
|
var certCount int
|
|
remaining := content
|
|
for len(remaining) > 0 {
|
|
block, rest := pem.Decode(remaining)
|
|
if block == nil {
|
|
break
|
|
}
|
|
|
|
if block.Type != "CERTIFICATE" {
|
|
t.Errorf("Expected block type 'CERTIFICATE', got %s", block.Type)
|
|
}
|
|
|
|
certCount++
|
|
remaining = rest
|
|
}
|
|
|
|
if certCount != 1 {
|
|
t.Errorf("Expected 1 certificate, found %d", certCount)
|
|
}
|
|
} |