mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-30 00:27:04 +09:00
The Credential view lists the account Secrets of the namespace by label and skips any no live HelmRelease owns, so no kind is served until its definition carries a credentials block. A mint replaces the stored value in one conditional write keyed on the Secret's resourceVersion and returns the plaintext once; a revoke discards it. Both record an Event on the application, which needs create on events. The application registry seeds an account Secret per declared account before it writes the HelmRelease, so the chart can reference them from the first render, prunes the Secrets of removed accounts after the write, and refuses to delete an application whose release is suspended, since Flux skips the uninstall and would leave the database running with the Secrets gone. The ApplicationDefinition reconciler computes the per-account valuesFrom entries with the function the API uses, so a release that predates the block gets them on its next reconcile. Schema defaulting moves to a package both can import. A definition declaring a format or an account source that is not built leaves its kind unserved rather than served with passwords the API cannot issue. Tenant roles gain get and list on credentials at view and create on mint and revoke at admin and for the tenant ServiceAccount. Attribution comes from the kube-apiserver audit log, so the operations docs get a policy for distributions that ship none, and monitoring-agents a value for an extra fluent-bit output so a SIEM can receive the log without dropping the VictoriaLogs outputs. Assisted-by: LLM Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
563 lines
19 KiB
Go
563 lines
19 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)
|
|
coreV1alpha1Storage["credentials/mint"] = credentialstorage.NewMintREST(w)
|
|
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 got["credentials/mint"] != "core.cozystack.io" {
|
|
t.Fatalf("expected the subresource registration to be gated, 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)
|
|
}
|
|
}
|