install template integrity test — render, config keys, scriptlet syntax
This commit is contained in:
parent
76fd491849
commit
34d4a02826
1 changed files with 237 additions and 0 deletions
237
server/internal/services/install_template_integrity_test.go
Normal file
237
server/internal/services/install_template_integrity_test.go
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// TemplateRenderData is the union of all fields across the three install
|
||||
// template data structs. Template execution fails on unknown variables, so
|
||||
// this test data exists for the sole purpose of verifying every {{.Field}}
|
||||
// in the template resolves.
|
||||
type TemplateRenderData struct {
|
||||
AgentID string
|
||||
BinaryURL string
|
||||
ConfigURL string
|
||||
Platform string
|
||||
Architecture string
|
||||
Version string
|
||||
ServerURL string
|
||||
RegistrationToken string
|
||||
AgentUser string
|
||||
AgentHome string
|
||||
ConfigDir string
|
||||
LogDir string
|
||||
AgentConfigDir string
|
||||
AgentLogDir string
|
||||
ServerKeyDir string
|
||||
ServerPublicKey string
|
||||
}
|
||||
|
||||
func TestInstallTemplateRenders(t *testing.T) {
|
||||
data := TemplateRenderData{
|
||||
AgentID: "00000000-0000-0000-0000-000000000000",
|
||||
BinaryURL: "https://example.com/redflag-agent",
|
||||
ConfigURL: "https://example.com/config",
|
||||
Platform: "linux",
|
||||
Architecture: "amd64",
|
||||
Version: "0.2.9.0",
|
||||
ServerURL: "https://example.com",
|
||||
RegistrationToken: "rt_test",
|
||||
AgentUser: "redflag-agent",
|
||||
AgentHome: "/var/lib/redflag/agent",
|
||||
ConfigDir: "/etc/redflag",
|
||||
LogDir: "/var/log/redflag",
|
||||
AgentConfigDir: "/etc/redflag/agent",
|
||||
AgentLogDir: "/var/log/redflag/agent",
|
||||
ServerKeyDir: "/etc/redflag/server",
|
||||
ServerPublicKey: "abcdef0123456789",
|
||||
}
|
||||
|
||||
scripts := []string{
|
||||
"templates/install/scripts/linux.sh.tmpl",
|
||||
"templates/install/scripts/windows.ps1.tmpl",
|
||||
}
|
||||
|
||||
for _, name := range scripts {
|
||||
t.Run(filepath.Base(name), func(t *testing.T) {
|
||||
tmpl, err := template.ParseFS(installScriptTemplates, name)
|
||||
if err != nil {
|
||||
t.Fatalf("parse template %s: %v", name, err)
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
t.Fatalf("render template %s: %v", name, err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
if len(output) < 100 {
|
||||
t.Fatalf("rendered template %s too short (%d bytes): likely empty or error", name, len(output))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreshInstallConfigKeys verifies the install template's default JSON
|
||||
// includes the config keys the agent expects at minimum. This catches drift
|
||||
// where a new config struct field gets added but the template never writes it
|
||||
// for fresh installs.
|
||||
//
|
||||
// THIS TEST IS NOT EXHAUSTIVE. It maintains a canonical list of keys that must
|
||||
// appear in the fresh-install config template. Add new keys here when they are
|
||||
// added to the agent Config struct and the template should set them.
|
||||
func TestFreshInstallConfigKeys(t *testing.T) {
|
||||
// Read and parse the linux.sh.tmpl to extract its default JSON block.
|
||||
raw, err := installScriptTemplates.ReadFile("templates/install/scripts/linux.sh.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("read template: %v", err)
|
||||
}
|
||||
source := string(raw)
|
||||
|
||||
// Extract the default JSON between the heredoc markers.
|
||||
// The template writes: sudo tee "..." > /dev/null <<EOF
|
||||
// Find the heredoc marker line, then the first `{` on the line after it.
|
||||
heredoc := `sudo tee "${AGENT_CONFIG_DIR}/config.json" > /dev/null <<EOF`
|
||||
heredocIdx := strings.Index(source, heredoc)
|
||||
if heredocIdx == -1 {
|
||||
t.Fatal("default JSON heredoc marker not found in install template")
|
||||
}
|
||||
// Skip past the <<EOF marker and the newline that follows.
|
||||
afterHeredoc := heredocIdx + len(heredoc)
|
||||
jsonStart := strings.Index(source[afterHeredoc:], "\n{")
|
||||
if jsonStart == -1 {
|
||||
t.Fatal("JSON start (\\n{) not found after heredoc marker")
|
||||
}
|
||||
jsonBlockStart := afterHeredoc + jsonStart + 1 // skip the newline
|
||||
jsonEnd := strings.Index(source[jsonBlockStart:], "\nEOF")
|
||||
if jsonEnd == -1 {
|
||||
t.Fatal("JSON end (\\nEOF) not found after heredoc")
|
||||
}
|
||||
jsonBlock := source[jsonBlockStart : jsonBlockStart+jsonEnd]
|
||||
|
||||
// Parse the JSON block and collect top-level keys.
|
||||
var parsed map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(jsonBlock), &parsed); err != nil {
|
||||
t.Fatalf("parse default JSON: %v\nblock:\n%s", err, jsonBlock)
|
||||
}
|
||||
|
||||
// Canonical required keys — every Config struct field with a json tag
|
||||
// that MUST appear in the fresh-install template. Keys with "omitempty"
|
||||
// or that default to zero-value-safe are optional and not listed here.
|
||||
required := []string{
|
||||
"version",
|
||||
"agent_version",
|
||||
"agent_id",
|
||||
"token",
|
||||
"refresh_token",
|
||||
"registration_token",
|
||||
"check_in_interval",
|
||||
"server_url",
|
||||
"network",
|
||||
"proxy",
|
||||
"tls",
|
||||
"logging",
|
||||
"subsystems",
|
||||
}
|
||||
// "machine_id" in the template JSON is a stale key not present in the
|
||||
// Config struct — the agent reads machine identity from hostname/TOFU.
|
||||
// Not a required key; retained in the template for backward compat with
|
||||
// config files that carry it.
|
||||
|
||||
for _, key := range required {
|
||||
if _, exists := parsed[key]; !exists {
|
||||
t.Errorf("required config key %q missing from fresh-install template JSON", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Optional keys — nice-to-have in the template but not required for
|
||||
// correct operation (zero values are handled by the agent).
|
||||
recommended := []string{
|
||||
"desktop",
|
||||
"polling",
|
||||
"command_signing",
|
||||
"security_logging",
|
||||
"security",
|
||||
}
|
||||
|
||||
for _, key := range recommended {
|
||||
if _, exists := parsed[key]; !exists {
|
||||
t.Logf("[INFO] recommended config key %q not in template — fresh installs will use defaults", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInstallTemplateScriptletSyntax performs a basic syntax sanity check on the
|
||||
// shell script portion of the install template — enough to catch common errors
|
||||
// like unterminated heredocs, unbalanced braces, or missing shell tokens.
|
||||
func TestInstallTemplateScriptletSyntax(t *testing.T) {
|
||||
|
||||
// Render with test data first to substitute template variables.
|
||||
data := TemplateRenderData{
|
||||
AgentID: "00000000-0000-0000-0000-000000000000",
|
||||
BinaryURL: "https://example.com/binary",
|
||||
ConfigURL: "https://example.com/config",
|
||||
Platform: "linux",
|
||||
Architecture: "amd64",
|
||||
Version: "0.2.9.0",
|
||||
ServerURL: "https://example.com",
|
||||
RegistrationToken: "rt_test",
|
||||
AgentUser: "redflag-agent",
|
||||
AgentHome: "/var/lib/redflag/agent",
|
||||
ConfigDir: "/etc/redflag",
|
||||
LogDir: "/var/log/redflag",
|
||||
AgentConfigDir: "/etc/redflag/agent",
|
||||
AgentLogDir: "/var/log/redflag/agent",
|
||||
ServerKeyDir: "/etc/redflag/server",
|
||||
ServerPublicKey: "abcdef0123456789",
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFS(installScriptTemplates, "templates/install/scripts/linux.sh.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("parse template: %v", err)
|
||||
}
|
||||
var buf strings.Builder
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
t.Fatalf("render template: %v", err)
|
||||
}
|
||||
|
||||
rendered := buf.String()
|
||||
|
||||
// Check major sections are present.
|
||||
sections := []string{
|
||||
"#!/bin/bash",
|
||||
"set -e",
|
||||
"systemctl",
|
||||
"sudoers",
|
||||
"config.json",
|
||||
"redflag-helper",
|
||||
"redflag-desktop",
|
||||
}
|
||||
for _, s := range sections {
|
||||
if !strings.Contains(rendered, s) {
|
||||
t.Errorf("rendered template missing expected section: %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// Rough heredoc balance check — count open/close markers.
|
||||
heredocOpens := strings.Count(rendered, "<<EOF")
|
||||
heredocCloses := strings.Count(rendered, "EOF")
|
||||
if heredocCloses < heredocOpens {
|
||||
t.Errorf("possible unterminated heredoc: %d <<EOF markers but only %d EOF terminators", heredocOpens, heredocCloses)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMain ensures the working directory is the server package root
|
||||
// so the embedded templates resolve correctly.
|
||||
func TestMain(m *testing.M) {
|
||||
// Allow override for test runner flexibility.
|
||||
if d := os.Getenv("REDFLAG_SERVER_DIR"); d != "" {
|
||||
os.Chdir(d)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
Loading…
Reference in a new issue