package main import ( "bytes" "context" "os" "path/filepath" "testing" "golang.org/x/crypto/acme/autocert" ) // testCertCacheConformance is the contract every cache backend (dir today, S3 // later) must satisfy — autocert and the wildcard manager rely on all three: // Get/Put round-trip, ErrCacheMiss (not a bare error) for an absent key, and // idempotent Delete. func testCertCacheConformance(t *testing.T, cache autocert.Cache) { t.Helper() ctx := context.Background() if _, err := cache.Get(ctx, "absent"); err != autocert.ErrCacheMiss { t.Fatalf("Get(absent) = %v, want ErrCacheMiss", err) } want := []byte("pem-bytes") if err := cache.Put(ctx, "chat.example.com", want); err != nil { t.Fatal(err) } if got, err := cache.Get(ctx, "chat.example.com"); err != nil || !bytes.Equal(got, want) { t.Fatalf("Get after Put = %q, %v", got, err) } // Overwrite is the renewal path. if err := cache.Put(ctx, "chat.example.com", []byte("renewed")); err != nil { t.Fatal(err) } if got, _ := cache.Get(ctx, "chat.example.com"); string(got) != "renewed" { t.Fatalf("overwrite lost: %q", got) } if err := cache.Delete(ctx, "chat.example.com"); err != nil { t.Fatal(err) } if _, err := cache.Get(ctx, "chat.example.com"); err != autocert.ErrCacheMiss { t.Fatalf("Get after Delete = %v, want ErrCacheMiss", err) } if err := cache.Delete(ctx, "never-existed"); err != nil { t.Fatalf("Delete(absent) = %v, want nil", err) } } func TestNewCertCacheDirBackend(t *testing.T) { dir := t.TempDir() t.Setenv("LCHAT_CERT_CACHE", "") // unset = dir cache, err := newCertCache(dir) if err != nil { t.Fatal(err) } testCertCacheConformance(t, cache) // The dir backend must be byte-identical to the pre-seam layout: a key is // a plain file name inside the acme cache dir, 0600 (every entry may hold // private key material) — an existing /data/acme keeps working over a // restart with no migration. ctx := context.Background() if err := cache.Put(ctx, "chat.example.com", []byte("pem")); err != nil { t.Fatal(err) } path := filepath.Join(dir, "chat.example.com") if raw, err := os.ReadFile(path); err != nil || string(raw) != "pem" { t.Fatalf("on-disk layout drifted: %q, %v", raw, err) } if fi, err := os.Stat(path); err != nil || fi.Mode().Perm() != 0o600 { t.Fatalf("cert file mode = %v, err %v (want 0600)", fi.Mode(), err) } // And the reverse: a cert file an older binary wrote directly is served. if err := os.WriteFile(filepath.Join(dir, "old.example.com"), []byte("old"), 0o600); err != nil { t.Fatal(err) } if raw, err := cache.Get(ctx, "old.example.com"); err != nil || string(raw) != "old" { t.Fatalf("pre-seam file not readable through the cache: %q, %v", raw, err) } } func TestNewCertCacheEnvSelection(t *testing.T) { t.Setenv("LCHAT_CERT_CACHE", "dir") if _, err := newCertCache(t.TempDir()); err != nil { t.Fatalf("explicit dir backend: %v", err) } // s3 is reserved: refusing to start beats a router silently minting certs // into a local dir the rest of a pool can't see. t.Setenv("LCHAT_CERT_CACHE", "s3") if _, err := newCertCache(t.TempDir()); err == nil { t.Fatal("s3 backend accepted before it exists") } t.Setenv("LCHAT_CERT_CACHE", "floppy") if _, err := newCertCache(t.TempDir()); err == nil { t.Fatal("unknown backend accepted") } }