mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-23 03:27:50 +09:00
branch-review on #3184 measured a real regression: switching CRD/ APIService discovery to content-based scanning made every file in packages/+internal a candidate, and each was split into documents and fully unmarshaled twice — once per parser — inflating a full snapshot load from ~0.5s to ~3.4s. Split each file's documents once and probe each document's kind once before deciding which shape to try, so the common case (a document that is neither a CRD nor an APIService — the overwhelming majority of what the wider walk now sees) pays for at most one parse attempt instead of two. Add a raw substring fast-reject, at both the per-document and whole-file level, ahead of any YAML parsing: neither kind this package cares about can appear without its own name appearing as a literal substring first. Also fixes two smaller gaps flagged in the same review: an untrusted PR head checkout could point discovery at an arbitrarily large file with no size guard (capped at 8 MiB), and a group's merged APIService resource only recorded the first contributing file's path, dropping the others from the report. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
559 lines
18 KiB
Go
559 lines
18 KiB
Go
/*
|
|
Copyright 2026 The Cozystack Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package apigate
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const sampleCozyRD = `apiVersion: cozystack.io/v1alpha1
|
|
kind: ApplicationDefinition
|
|
metadata:
|
|
name: postgres
|
|
spec:
|
|
application:
|
|
kind: Postgres
|
|
singular: postgres
|
|
plural: postgreses
|
|
openAPISchema: |-
|
|
{"type":"object","properties":{"replicas":{"type":"integer","default":2}}}
|
|
`
|
|
|
|
const sampleCRD = `apiVersion: apiextensions.k8s.io/v1
|
|
kind: CustomResourceDefinition
|
|
metadata:
|
|
name: widgets.example.cozystack.io
|
|
spec:
|
|
group: example.cozystack.io
|
|
names:
|
|
kind: Widget
|
|
plural: widgets
|
|
scope: Namespaced
|
|
versions:
|
|
- name: v1alpha1
|
|
served: true
|
|
storage: true
|
|
schema:
|
|
openAPIV3Schema:
|
|
type: object
|
|
properties:
|
|
spec:
|
|
type: object
|
|
properties:
|
|
size:
|
|
type: string
|
|
`
|
|
|
|
func TestParseCozyRD(t *testing.T) {
|
|
r, ok, err := ParseCozyRD("postgres.yaml", []byte(sampleCozyRD))
|
|
if err != nil || !ok {
|
|
t.Fatalf("ParseCozyRD failed: ok=%v err=%v", ok, err)
|
|
}
|
|
if r.Group != "apps.cozystack.io" || r.Kind != "Postgres" || r.Plural != "postgreses" {
|
|
t.Fatalf("unexpected identity: %+v", r)
|
|
}
|
|
if _, ok := r.Versions["v1alpha1"]; !ok {
|
|
t.Fatalf("expected v1alpha1 schema, got versions %v", r.Versions)
|
|
}
|
|
}
|
|
|
|
func TestParseCRDs(t *testing.T) {
|
|
rs, err := ParseCRDs("widget.yaml", []byte(sampleCRD))
|
|
if err != nil {
|
|
t.Fatalf("ParseCRDs failed: %v", err)
|
|
}
|
|
if len(rs) != 1 {
|
|
t.Fatalf("expected 1 CRD resource, got %d", len(rs))
|
|
}
|
|
r := rs[0]
|
|
if r.Group != "example.cozystack.io" || r.Kind != "Widget" || r.Plural != "widgets" {
|
|
t.Fatalf("unexpected identity: %+v", r)
|
|
}
|
|
if _, ok := r.Versions["v1alpha1"]; !ok {
|
|
t.Fatalf("expected v1alpha1 schema")
|
|
}
|
|
}
|
|
|
|
// TestParseCRDsSkipsUnservedVersions covers B7(a): a version flipped to
|
|
// served:false is not live API surface and must be excluded, so the standard
|
|
// CRD deprecation step surfaces later as a removed served version.
|
|
func TestParseCRDsSkipsUnservedVersions(t *testing.T) {
|
|
const crd = `apiVersion: apiextensions.k8s.io/v1
|
|
kind: CustomResourceDefinition
|
|
metadata:
|
|
name: widgets.example.cozystack.io
|
|
spec:
|
|
group: example.cozystack.io
|
|
names: {kind: Widget, plural: widgets}
|
|
versions:
|
|
- name: v1alpha1
|
|
served: false
|
|
schema: {openAPIV3Schema: {type: object}}
|
|
- name: v1beta1
|
|
served: true
|
|
schema: {openAPIV3Schema: {type: object}}
|
|
`
|
|
rs, err := ParseCRDs("widget.yaml", []byte(crd))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rs) != 1 {
|
|
t.Fatalf("expected 1 resource, got %d", len(rs))
|
|
}
|
|
if _, ok := rs[0].Versions["v1alpha1"]; ok {
|
|
t.Fatalf("unserved v1alpha1 must be excluded, got versions %v", rs[0].Versions)
|
|
}
|
|
if _, ok := rs[0].Versions["v1beta1"]; !ok {
|
|
t.Fatalf("served v1beta1 must be present, got versions %v", rs[0].Versions)
|
|
}
|
|
}
|
|
|
|
// TestParseCRDsToleratesTemplatedNeighborDocument reproduces the cozyplane
|
|
// bug: a real CRD living in a chart's templates/ dir, guarded by a
|
|
// `{{- if }}` block that is not valid YAML on its own. The malformed
|
|
// neighbor document must not take the real CRD down with it.
|
|
func TestParseCRDsToleratesTemplatedNeighborDocument(t *testing.T) {
|
|
const doc = `{{- /* CRDs are installed only in CRD mode */ -}}
|
|
{{- if not .Values.apiserver.enabled }}
|
|
---
|
|
apiVersion: apiextensions.k8s.io/v1
|
|
kind: CustomResourceDefinition
|
|
metadata:
|
|
name: vpcs.sdn.cozystack.io
|
|
spec:
|
|
group: sdn.cozystack.io
|
|
names: {kind: VPC, plural: vpcs}
|
|
versions:
|
|
- name: v1alpha1
|
|
served: true
|
|
schema: {openAPIV3Schema: {type: object}}
|
|
{{- end }}
|
|
`
|
|
rs, err := ParseCRDs("templates/crds.yaml", []byte(doc))
|
|
if err != nil {
|
|
t.Fatalf("ParseCRDs failed: %v", err)
|
|
}
|
|
if len(rs) != 1 || rs[0].Plural != "vpcs" {
|
|
t.Fatalf("expected the CRD sharing the file with templated markup to survive, got %v", rs)
|
|
}
|
|
}
|
|
|
|
// TestParseAPIServices covers the new APIService source: a first-party group
|
|
// registration is surfaced as a schema-less resource, and a non-APIService or
|
|
// incomplete document is ignored.
|
|
func TestParseAPIServices(t *testing.T) {
|
|
const doc = `apiVersion: apiregistration.k8s.io/v1
|
|
kind: APIService
|
|
metadata:
|
|
name: v1alpha1.sdn.cozystack.io
|
|
spec:
|
|
group: sdn.cozystack.io
|
|
version: v1alpha1
|
|
groupPriorityMinimum: 1000
|
|
versionPriority: 10
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: cozyplane-apiserver
|
|
`
|
|
rs := ParseAPIServices("apiserver.yaml", []byte(doc))
|
|
if len(rs) != 1 {
|
|
t.Fatalf("expected exactly 1 APIService resource, got %v", rs)
|
|
}
|
|
r := rs[0]
|
|
if r.Group != "sdn.cozystack.io" || r.Source != SourceAPIService || r.Plural != apiServicePlural {
|
|
t.Fatalf("unexpected APIService resource: %+v", r)
|
|
}
|
|
if _, ok := r.Versions["v1alpha1"]; !ok {
|
|
t.Fatalf("expected v1alpha1 in versions, got %v", r.Versions)
|
|
}
|
|
}
|
|
|
|
func TestParseAPIServerStorages(t *testing.T) {
|
|
src := `
|
|
coreV1alpha1Storage["tenantsecrets"] = cozyregistry.RESTInPeace(x)
|
|
sdnV1alpha1Storage["securitygroups"] = cozyregistry.RESTInPeace(y)
|
|
appsV1alpha1Storage[resConfig.Application.Plural] = cozyregistry.RESTInPeace(z)
|
|
`
|
|
rs := ParseAPIServerStorages("apiserver.go", []byte(src))
|
|
got := map[string]string{}
|
|
for _, r := range rs {
|
|
got[r.Plural] = r.Group
|
|
}
|
|
if got["tenantsecrets"] != "core.cozystack.io" {
|
|
t.Fatalf("expected core tenantsecrets, got %v", got)
|
|
}
|
|
if got["securitygroups"] != "sdn.cozystack.io" {
|
|
t.Fatalf("expected sdn securitygroups, got %v", got)
|
|
}
|
|
if _, leaked := got["Plural"]; leaked {
|
|
t.Fatalf("apps dynamic registration must not be captured: %v", got)
|
|
}
|
|
}
|
|
|
|
// TestParseAPIServerStoragesSurfacesUnknownPrefix covers the drift guard: a new
|
|
// aggregated group registered under an unlisted *Storage variable must not be
|
|
// silently dropped, or it would bypass the gate entirely.
|
|
func TestParseAPIServerStoragesSurfacesUnknownPrefix(t *testing.T) {
|
|
src := `newgroupV1alpha1Storage["foos"] = cozyregistry.RESTInPeace(x)`
|
|
rs := ParseAPIServerStorages("apiserver.go", []byte(src))
|
|
if len(rs) != 1 {
|
|
t.Fatalf("expected the unknown-prefix resource to be surfaced, got %v", rs)
|
|
}
|
|
if !strings.HasPrefix(rs[0].Group, unmappedStoragePrefix) {
|
|
t.Fatalf("unknown prefix must surface under the unmapped group, got %q", rs[0].Group)
|
|
}
|
|
}
|
|
|
|
// TestLoadSnapshotDiscoversCRDsUnderChartsCrds covers B6: a first-party CRD that
|
|
// lives in a chart's crds/ directory (rather than a definitions/ dir) is still
|
|
// discovered.
|
|
func TestLoadSnapshotDiscoversCRDsUnderChartsCrds(t *testing.T) {
|
|
root := t.TempDir()
|
|
write := func(rel, content string) {
|
|
p := filepath.Join(root, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write("packages/system/foo/charts/foo/crds/example.cozystack.io_widgets.yaml", sampleCRD)
|
|
|
|
snap, err := LoadSnapshot(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := snap[resourceKey{Group: "example.cozystack.io", Plural: "widgets"}]; !ok {
|
|
t.Fatalf("CRD under charts/*/crds/ was not discovered: %v", snap)
|
|
}
|
|
}
|
|
|
|
// TestLoadSnapshotDiscoversCRDUnderTemplatesDir reproduces the real gap found
|
|
// in the cozyplane PR: a raw CRD document guarded by `{{- if }}`, sitting in a
|
|
// chart's templates/ dir (not crds/), must still be discovered — directory
|
|
// name is no longer the discovery signal, file content is.
|
|
func TestLoadSnapshotDiscoversCRDUnderTemplatesDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
write := func(rel, content string) {
|
|
p := filepath.Join(root, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write("packages/system/cozyplane/templates/crds.yaml", `{{- if not .Values.apiserver.enabled }}
|
|
---
|
|
apiVersion: apiextensions.k8s.io/v1
|
|
kind: CustomResourceDefinition
|
|
metadata:
|
|
name: vpcs.sdn.cozystack.io
|
|
spec:
|
|
group: sdn.cozystack.io
|
|
names: {kind: VPC, plural: vpcs}
|
|
versions:
|
|
- name: v1alpha1
|
|
served: true
|
|
schema: {openAPIV3Schema: {type: object}}
|
|
{{- end }}
|
|
`)
|
|
|
|
snap, err := LoadSnapshot(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := snap[resourceKey{Group: "sdn.cozystack.io", Plural: "vpcs"}]; !ok {
|
|
t.Fatalf("CRD under templates/ was not discovered: %v", snap)
|
|
}
|
|
}
|
|
|
|
// TestLoadSnapshotDiscoversAPIServiceAndTripsNewGroup covers the other half of
|
|
// the cozyplane gap: a group served only by an aggregated apiserver (no CRD,
|
|
// no cozyrd, no apiserver.go entry) — its schema lives entirely outside this
|
|
// repo, but the group's first appearance must still require review. Versions
|
|
// registered via separate sibling APIService objects (one per version, the
|
|
// real-world shape) must merge onto the same resource.
|
|
func TestLoadSnapshotDiscoversAPIServiceAndTripsNewGroup(t *testing.T) {
|
|
root := t.TempDir()
|
|
write := func(rel, content string) {
|
|
p := filepath.Join(root, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write("packages/system/cozyplane/templates/apiserver-v1alpha1.yaml", `apiVersion: apiregistration.k8s.io/v1
|
|
kind: APIService
|
|
metadata: {name: v1alpha1.sdn.cozystack.io}
|
|
spec: {group: sdn.cozystack.io, version: v1alpha1}
|
|
`)
|
|
write("packages/system/cozyplane/templates/apiserver-v1beta1.yaml", `apiVersion: apiregistration.k8s.io/v1
|
|
kind: APIService
|
|
metadata: {name: v1beta1.sdn.cozystack.io}
|
|
spec: {group: sdn.cozystack.io, version: v1beta1}
|
|
`)
|
|
|
|
// A minimal but non-empty base, unrelated to sdn.cozystack.io.
|
|
baseRoot := t.TempDir()
|
|
writeBase := func(rel, content string) {
|
|
p := filepath.Join(baseRoot, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
writeBase("packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
|
|
|
|
base, err := LoadSnapshot(baseRoot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
head, err := LoadSnapshot(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
key := resourceKey{Group: "sdn.cozystack.io", Plural: apiServicePlural}
|
|
res, ok := head[key]
|
|
if !ok {
|
|
t.Fatalf("APIService group was not discovered: %v", head)
|
|
}
|
|
if _, ok := res.Versions["v1alpha1"]; !ok {
|
|
t.Fatalf("expected v1alpha1 merged in, got %v", res.Versions)
|
|
}
|
|
if _, ok := res.Versions["v1beta1"]; !ok {
|
|
t.Fatalf("expected v1beta1 from the sibling APIService object merged in, got %v", res.Versions)
|
|
}
|
|
|
|
if countCategory(Classify(base, head), NewGroup) != 1 {
|
|
t.Fatalf("expected the APIService-only group to trip NewGroup, got %v", Classify(base, head))
|
|
}
|
|
}
|
|
|
|
// TestLoadSnapshotIgnoresThirdPartyManifestsUnderTemplates guards against the
|
|
// content-based walk becoming too permissive: a vendored CRD and APIService
|
|
// for a non-cozystack.io group, sitting in the exact same templates/ shape,
|
|
// must still be filtered out.
|
|
func TestLoadSnapshotIgnoresThirdPartyManifestsUnderTemplates(t *testing.T) {
|
|
root := t.TempDir()
|
|
write := func(rel, content string) {
|
|
p := filepath.Join(root, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write("packages/system/cert-manager/templates/crds.yaml", `apiVersion: apiextensions.k8s.io/v1
|
|
kind: CustomResourceDefinition
|
|
metadata: {name: certificates.cert-manager.io}
|
|
spec:
|
|
group: cert-manager.io
|
|
names: {kind: Certificate, plural: certificates}
|
|
versions:
|
|
- name: v1
|
|
served: true
|
|
schema: {openAPIV3Schema: {type: object}}
|
|
`)
|
|
write("packages/system/metrics/templates/apiservice.yaml", `apiVersion: apiregistration.k8s.io/v1
|
|
kind: APIService
|
|
metadata: {name: v1beta1.metrics.k8s.io}
|
|
spec: {group: metrics.k8s.io, version: v1beta1}
|
|
`)
|
|
// Give the checkout a first-party resource so LoadSnapshot's
|
|
// empty-snapshot guard doesn't fire for an unrelated reason.
|
|
write("packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
|
|
|
|
snap, err := LoadSnapshot(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := snap[resourceKey{Group: "cert-manager.io", Plural: "certificates"}]; ok {
|
|
t.Fatalf("vendored third-party CRD must not be discovered: %v", snap)
|
|
}
|
|
if _, ok := snap[resourceKey{Group: "metrics.k8s.io", Plural: apiServicePlural}]; ok {
|
|
t.Fatalf("vendored third-party APIService must not be discovered: %v", snap)
|
|
}
|
|
}
|
|
|
|
// TestLoadSnapshotSkipsOversizedFiles guards the size cap added alongside
|
|
// content-based discovery: since every .yaml/.yml file in the tree is now a
|
|
// candidate (no directory allowlist), an untrusted PR head checkout could
|
|
// otherwise force an unbounded read/parse of a pathologically large file. A
|
|
// first-party CRD past maxYAMLFileSize must be excluded from discovery.
|
|
func TestLoadSnapshotSkipsOversizedFiles(t *testing.T) {
|
|
root := t.TempDir()
|
|
write := func(rel, content string) {
|
|
p := filepath.Join(root, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
padding := strings.Repeat("# padding\n", (maxYAMLFileSize/len("# padding\n"))+1)
|
|
write("packages/system/oversized/crds/widgets.yaml", padding+sampleCRD)
|
|
// A small, legitimate resource so the empty-snapshot guard doesn't fire
|
|
// for an unrelated reason.
|
|
write("packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
|
|
|
|
snap, err := LoadSnapshot(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := snap[resourceKey{Group: "example.cozystack.io", Plural: "widgets"}]; ok {
|
|
t.Fatalf("CRD in a file past maxYAMLFileSize must be excluded: %v", snap)
|
|
}
|
|
}
|
|
|
|
// TestLoadSnapshotMergesAPIServiceOriginAcrossFiles covers the traceability
|
|
// fix: when a group's versions are registered via several sibling APIService
|
|
// objects in different files (the real-world shape — one object per
|
|
// version), the merged resource's Origin must mention every contributing
|
|
// file, not just the first one encountered.
|
|
func TestLoadSnapshotMergesAPIServiceOriginAcrossFiles(t *testing.T) {
|
|
root := t.TempDir()
|
|
write := func(rel, content string) {
|
|
p := filepath.Join(root, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write("packages/system/cozyplane/templates/apiserver-v1alpha1.yaml", `apiVersion: apiregistration.k8s.io/v1
|
|
kind: APIService
|
|
metadata: {name: v1alpha1.sdn.cozystack.io}
|
|
spec: {group: sdn.cozystack.io, version: v1alpha1}
|
|
`)
|
|
write("packages/system/cozyplane/templates/apiserver-v1beta1.yaml", `apiVersion: apiregistration.k8s.io/v1
|
|
kind: APIService
|
|
metadata: {name: v1beta1.sdn.cozystack.io}
|
|
spec: {group: sdn.cozystack.io, version: v1beta1}
|
|
`)
|
|
|
|
snap, err := LoadSnapshot(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, ok := snap[resourceKey{Group: "sdn.cozystack.io", Plural: apiServicePlural}]
|
|
if !ok {
|
|
t.Fatalf("APIService group was not discovered: %v", snap)
|
|
}
|
|
for _, want := range []string{
|
|
"packages/system/cozyplane/templates/apiserver-v1alpha1.yaml",
|
|
"packages/system/cozyplane/templates/apiserver-v1beta1.yaml",
|
|
} {
|
|
if !strings.Contains(res.Origin, want) {
|
|
t.Fatalf("expected Origin to mention %q, got %q", want, res.Origin)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkLoadSnapshotRealTree tracks LoadSnapshot's wall-clock cost against
|
|
// this repository's own checkout — the actual worst case the CI gate runs
|
|
// against on every PR. Content-based discovery (see discoverYAMLFiles) scans
|
|
// every .yaml/.yml file under packages/ and internal/, not a curated
|
|
// directory allowlist, so this is meant to catch a future change that
|
|
// reintroduces redundant per-file work (e.g. splitting or parsing the same
|
|
// file's documents more than once). Run with:
|
|
//
|
|
// go test ./internal/apigate/ -run '^$' -bench LoadSnapshotRealTree
|
|
func BenchmarkLoadSnapshotRealTree(b *testing.B) {
|
|
for b.Loop() {
|
|
if _, err := LoadSnapshot("../.."); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestLoadSnapshotEmptyErrors covers the silent-bypass guard: an empty result
|
|
// (e.g. wrong directory) must be an error, not a clean pass.
|
|
func TestLoadSnapshotEmptyErrors(t *testing.T) {
|
|
if _, err := LoadSnapshot(t.TempDir()); err == nil {
|
|
t.Fatal("expected an error for a directory with no API resources")
|
|
}
|
|
}
|
|
|
|
// TestIsFirstPartyGroup covers the subdomain check: only cozystack.io and its
|
|
// dot-delimited subdomains are first-party; a lookalike suffix is not.
|
|
func TestIsFirstPartyGroup(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"cozystack.io": true,
|
|
"apps.cozystack.io": true,
|
|
"strategy.backups.cozystack.io": true,
|
|
"fakecozystack.io": false,
|
|
"cozystack.io.evil.com": false,
|
|
"cert-manager.io": false,
|
|
}
|
|
for group, want := range cases {
|
|
if got := isFirstPartyGroup(group); got != want {
|
|
t.Errorf("isFirstPartyGroup(%q) = %v, want %v", group, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestLoadSnapshotEndToEnd builds two minimal checkouts on disk and verifies
|
|
// the loader + classifier wire together across all three sources.
|
|
func TestLoadSnapshotEndToEnd(t *testing.T) {
|
|
base := t.TempDir()
|
|
head := t.TempDir()
|
|
|
|
write := func(root, rel, content string) {
|
|
p := filepath.Join(root, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Base has just Postgres.
|
|
write(base, "packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
|
|
// Head keeps Postgres and adds a brand-new CRD group.
|
|
write(head, "packages/system/postgres-rd/cozyrds/postgres.yaml", sampleCozyRD)
|
|
write(head, "internal/crdinstall/manifests/example.yaml", sampleCRD)
|
|
|
|
baseSnap, err := LoadSnapshot(base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
headSnap, err := LoadSnapshot(head)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
findings := Classify(baseSnap, headSnap)
|
|
if countCategory(findings, NewGroup) != 1 {
|
|
t.Fatalf("expected 1 new-group finding, got %v", findings)
|
|
}
|
|
}
|