mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
Ghost+MySQL, Gitea+PostgreSQL, Nextcloud+PG+Redis, Paperless+PG+Redis, Plausible+PG+ClickHouse. TestOfficialTemplatesAllParse parses every seed, topo-sorts graphs, and dry-resolves with no unresolved refs. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
203 lines
6.4 KiB
Go
203 lines
6.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTopoSortServicesOrdersByDependsOn(t *testing.T) {
|
|
sorted, err := topoSortServices([]TemplateServiceSpec{
|
|
{Key: "web", DependsOn: []string{"db"}},
|
|
{Key: "db", Type: "database"},
|
|
{Key: "worker", DependsOn: []string{"web", "db"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("topoSortServices: %v", err)
|
|
}
|
|
if sorted[0].Key != "db" || sorted[1].Key != "web" || sorted[2].Key != "worker" {
|
|
t.Fatalf("unexpected order: %v", []string{sorted[0].Key, sorted[1].Key, sorted[2].Key})
|
|
}
|
|
}
|
|
|
|
func TestTopoSortServicesRejectsCycle(t *testing.T) {
|
|
_, err := topoSortServices([]TemplateServiceSpec{
|
|
{Key: "a", DependsOn: []string{"b"}},
|
|
{Key: "b", DependsOn: []string{"a"}},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "cycle") {
|
|
t.Fatalf("expected cycle error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTopoSortServicesRejectsUnknownDep(t *testing.T) {
|
|
_, err := topoSortServices([]TemplateServiceSpec{
|
|
{Key: "a", DependsOn: []string{"ghost"}},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "unknown key") {
|
|
t.Fatalf("expected unknown-key error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveStaticExpr(t *testing.T) {
|
|
unresolved := []string{}
|
|
var generated bool
|
|
|
|
v := resolveStaticExpr("{{secret}}", nil, &unresolved, &generated)
|
|
if len(v) != 32 || !generated {
|
|
t.Fatalf("secret: got %q generated=%v", v, generated)
|
|
}
|
|
v = resolveStaticExpr("{{secret(8)}}", nil, &unresolved, &generated)
|
|
if len(v) != 16 {
|
|
t.Fatalf("secret(8): got %q", v)
|
|
}
|
|
v = resolveStaticExpr("port-{{random:10}}", nil, &unresolved, &generated)
|
|
if !strings.HasPrefix(v, "port-") {
|
|
t.Fatalf("random: got %q", v)
|
|
}
|
|
v = resolveStaticExpr("{{DOMAIN:-example.com}}", nil, &unresolved, &generated)
|
|
if v != "example.com" {
|
|
t.Fatalf("var default: got %q", v)
|
|
}
|
|
v = resolveStaticExpr("{{DOMAIN}}", map[string]string{"DOMAIN": "x.io"}, &unresolved, &generated)
|
|
if v != "x.io" {
|
|
t.Fatalf("var override: got %q", v)
|
|
}
|
|
v = resolveStaticExpr("{{MISSING}}", nil, &unresolved, &generated)
|
|
if v != "" || len(unresolved) == 0 || unresolved[0] != "MISSING" {
|
|
t.Fatalf("missing var: got %q unresolved=%v", v, unresolved)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateGraphDatabaseRefs(t *testing.T) {
|
|
sorted, err := topoSortServices([]TemplateServiceSpec{
|
|
{Key: "db", Type: "database", Runtime: "postgresql", Environment: map[string]string{
|
|
"POSTGRES_DB": "shop",
|
|
}},
|
|
{Key: "web", Type: "web", Runtime: "nginx", DependsOn: []string{"db"}, Environment: map[string]string{
|
|
"DATABASE_URL": "{{service.db.url}}",
|
|
"DB_HOST": "{{service.db.host}}",
|
|
"DB_PORT": "{{service.db.port}}",
|
|
"SESSION_TOKEN": "{{secret}}",
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("topo: %v", err)
|
|
}
|
|
graph := resolveTemplateGraph(sorted, nil)
|
|
if len(graph.unresolved) > 0 {
|
|
t.Fatalf("unresolved: %v", graph.unresolved)
|
|
}
|
|
web := graph.members[1]
|
|
if web.env["DB_HOST"] != "db" {
|
|
t.Fatalf("DB_HOST=%q", web.env["DB_HOST"])
|
|
}
|
|
if web.env["DB_PORT"] != "5432" {
|
|
t.Fatalf("DB_PORT=%q", web.env["DB_PORT"])
|
|
}
|
|
dbURL := web.env["DATABASE_URL"]
|
|
if !strings.HasPrefix(dbURL, "postgresql://") || !strings.Contains(dbURL, "@db:5432/shop") {
|
|
t.Fatalf("DATABASE_URL=%q", dbURL)
|
|
}
|
|
if len(web.env["SESSION_TOKEN"]) != 32 || !web.secretKeys["SESSION_TOKEN"] {
|
|
t.Fatalf("SESSION_TOKEN=%q secret=%v", web.env["SESSION_TOKEN"], web.secretKeys["SESSION_TOKEN"])
|
|
}
|
|
// Database member must carry the generated password so refs and the
|
|
// container agree.
|
|
db := graph.members[0]
|
|
if pw := db.env["POSTGRES_PASSWORD"]; len(pw) != 32 || !db.secretKeys["POSTGRES_PASSWORD"] {
|
|
t.Fatalf("POSTGRES_PASSWORD=%q secret=%v", pw, db.secretKeys["POSTGRES_PASSWORD"])
|
|
}
|
|
if !strings.Contains(dbURL, db.env["POSTGRES_PASSWORD"]) {
|
|
t.Fatalf("url %q does not carry the resolved password", dbURL)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateGraphServiceRefs(t *testing.T) {
|
|
sorted, err := topoSortServices([]TemplateServiceSpec{
|
|
{Key: "api", Type: "web", Runtime: "ghcr.io/x/api:1", Port: 8080},
|
|
{Key: "front", Type: "web", Runtime: "ghcr.io/x/front:1", Port: 3000, DependsOn: []string{"api"}, Environment: map[string]string{
|
|
"API_URL": "{{service.api.url}}/v2",
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("topo: %v", err)
|
|
}
|
|
graph := resolveTemplateGraph(sorted, nil)
|
|
if got := graph.members[1].env["API_URL"]; got != "http://api:8080/v2" {
|
|
t.Fatalf("API_URL=%q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateGraphUnresolvedServiceProp(t *testing.T) {
|
|
sorted, _ := topoSortServices([]TemplateServiceSpec{
|
|
{Key: "web", Type: "web", Runtime: "nginx", Environment: map[string]string{
|
|
"X": "{{service.nope.host}}",
|
|
}},
|
|
})
|
|
graph := resolveTemplateGraph(sorted, nil)
|
|
if len(graph.unresolved) == 0 {
|
|
t.Fatal("expected unresolved ref")
|
|
}
|
|
}
|
|
|
|
func TestValidateTemplateGraph(t *testing.T) {
|
|
if err := validateTemplateGraph([]TemplateServiceSpec{
|
|
{Key: "db", Type: "database", Runtime: "postgresql"},
|
|
}); err != nil {
|
|
t.Fatalf("valid db spec rejected: %v", err)
|
|
}
|
|
if err := validateTemplateGraph([]TemplateServiceSpec{
|
|
{Key: "db", Type: "database", Runtime: "nonsense"},
|
|
}); err == nil {
|
|
t.Fatal("expected unsupported db engine error")
|
|
}
|
|
if err := validateTemplateGraph([]TemplateServiceSpec{
|
|
{Key: "web", Type: "web"},
|
|
}); err == nil {
|
|
t.Fatal("expected missing runtime error")
|
|
}
|
|
}
|
|
|
|
// TestOfficialTemplatesAllParse walks every seeded template and asserts the
|
|
// config parses, graph templates topo-sort + validate, and a dry-run resolve
|
|
// produces no unresolved refs (defaults cover all expressions).
|
|
func TestOfficialTemplatesAllParse(t *testing.T) {
|
|
for _, tpl := range SeedTemplates() {
|
|
var cfg TemplateConfig
|
|
if err := json.Unmarshal([]byte(tpl.Config), &cfg); err != nil {
|
|
t.Errorf("%s: config parse: %v", tpl.ID, err)
|
|
continue
|
|
}
|
|
if len(cfg.Services) == 0 {
|
|
continue
|
|
}
|
|
sorted, err := topoSortServices(cfg.Services)
|
|
if err != nil {
|
|
t.Errorf("%s: topo sort: %v", tpl.ID, err)
|
|
continue
|
|
}
|
|
if err := validateTemplateGraph(sorted); err != nil {
|
|
t.Errorf("%s: validate: %v", tpl.ID, err)
|
|
continue
|
|
}
|
|
var vars []TemplateVariable
|
|
if tpl.Variables != "" {
|
|
if err := json.Unmarshal([]byte(tpl.Variables), &vars); err != nil {
|
|
t.Errorf("%s: variables parse: %v", tpl.ID, err)
|
|
continue
|
|
}
|
|
}
|
|
userVars := map[string]string{}
|
|
for _, v := range vars {
|
|
if v.Default != "" {
|
|
userVars[v.Key] = v.Default
|
|
}
|
|
}
|
|
graph := resolveTemplateGraph(sorted, userVars)
|
|
if len(graph.unresolved) > 0 {
|
|
t.Errorf("%s: unresolved refs: %v", tpl.ID, graph.unresolved)
|
|
}
|
|
}
|
|
}
|