Merge pull request 'feat: Instance CredentialReference 值对象与校验测试' (#8) from feature/instance-credential-reference into main
E2E Tests / Run on Ubuntu (push) Failing after 1m4s
Tests / Run on Ubuntu (push) Successful in 7m6s
Lint / Run on Ubuntu (push) Successful in 8m19s

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-09-13 15:55:31 +00:00
2 changed files with 186 additions and 0 deletions
@@ -0,0 +1,67 @@
/*
Copyright 2026.
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 instance
import (
"errors"
"regexp"
)
// CredentialReferenceValues contains effective field mappings, not secret data.
// The application supplies defaults and fixes the namespace to the controller's.
// Namespace and provider-specific paths are deliberately not selectable here.
type CredentialReferenceValues struct {
Name string
UsernameKey string
PasswordKey string
}
// CredentialReference is an immutable reference to a management Secret.
// Its zero value is invalid; aggregate construction must Validate incoming values.
type CredentialReference struct {
values CredentialReferenceValues
}
// Secret names follow the Kubernetes DNS subdomain syntax and 253-character limit.
var secretName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
func NewCredentialReference(values CredentialReferenceValues) (CredentialReference, error) {
reference := CredentialReference{values: values}
if err := reference.Validate(); err != nil {
return CredentialReference{}, err
}
return reference, nil
}
// Values returns a copy of the reference, never secret contents.
func (r CredentialReference) Values() CredentialReferenceValues { return r.values }
// Validate enforces reference invariants without accessing Kubernetes or OpenBao.
// Checking that the referenced Secret contains nonempty credentials is an application
// responsibility. Errors omit input values and no implicit defaults are applied.
func (r CredentialReference) Validate() error {
if len(r.values.Name) > 253 || !secretName.MatchString(r.values.Name) {
return errors.New("management Secret name must be a valid DNS subdomain of at most 253 characters")
}
if r.values.UsernameKey == "" {
return errors.New("management Secret username field is required")
}
if r.values.PasswordKey == "" {
return errors.New("management Secret password field is required")
}
return nil
}
@@ -0,0 +1,119 @@
/*
Copyright 2026.
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 instance_test
import (
"strings"
"testing"
"git.ddupan.top/panxiao81/postgresql-tenant-operator/internal/domain/instance"
)
func validCredentialReference() instance.CredentialReferenceValues {
return instance.CredentialReferenceValues{
Name: "shared-postgresql-admin", UsernameKey: "username", PasswordKey: "password",
}
}
// Acceptance: docs/domain-instance.md §2. References carry names, never credentials or IO.
func TestCredentialReferenceRejectsInvalidValues(t *testing.T) {
cases := []struct {
name string
change func(*instance.CredentialReferenceValues)
}{
{"empty name", func(v *instance.CredentialReferenceValues) { v.Name = "" }},
{"uppercase", func(v *instance.CredentialReferenceValues) { v.Name = "Admin" }},
{"underscore", func(v *instance.CredentialReferenceValues) { v.Name = "pg_admin" }},
{"leading hyphen", func(v *instance.CredentialReferenceValues) { v.Name = "-admin" }},
{"trailing hyphen", func(v *instance.CredentialReferenceValues) { v.Name = "admin-" }},
{"empty label", func(v *instance.CredentialReferenceValues) { v.Name = "pg..admin" }},
{"trailing dot", func(v *instance.CredentialReferenceValues) { v.Name = "pg." }},
{"namespace or path", func(v *instance.CredentialReferenceValues) { v.Name = "system/admin" }},
{"whitespace", func(v *instance.CredentialReferenceValues) { v.Name = " admin" }},
{"too long", func(v *instance.CredentialReferenceValues) { v.Name = strings.Repeat("a", 254) }},
{"empty username key", func(v *instance.CredentialReferenceValues) { v.UsernameKey = "" }},
{"empty password key", func(v *instance.CredentialReferenceValues) { v.PasswordKey = "" }},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
values := validCredentialReference()
tc.change(&values)
reference, err := instance.NewCredentialReference(values)
if err == nil {
t.Fatal("invalid credential reference accepted")
}
if reference != (instance.CredentialReference{}) {
t.Fatal("constructor returned a partial reference on failure")
}
})
}
}
func TestCredentialReferencePreservesExplicitValues(t *testing.T) {
for _, name := range []string{"a", "1", "pg.admin-1", strings.Repeat("a", 253)} {
values := validCredentialReference()
values.Name = name
values.UsernameKey = "PG_USER"
values.PasswordKey = "pg.password"
reference, err := instance.NewCredentialReference(values)
if err != nil {
t.Fatal(err)
}
if reference.Values() != values {
t.Fatal("constructor changed the explicit field mapping")
}
if err := reference.Validate(); err != nil {
t.Fatal(err)
}
}
}
func TestCredentialReferenceIsAnImmutableComparableValue(t *testing.T) {
values := validCredentialReference()
reference, err := instance.NewCredentialReference(values)
if err != nil {
t.Fatal(err)
}
same, err := instance.NewCredentialReference(values)
if err != nil {
t.Fatal(err)
}
if reference != same {
t.Fatal("identical references must compare equal")
}
values.Name = "different"
snapshot := reference.Values()
snapshot.PasswordKey = "different-key"
if reference.Values() != validCredentialReference() {
t.Fatal("caller mutated reference through a copy")
}
if err := (instance.CredentialReference{}).Validate(); err == nil {
t.Fatal("zero reference must be invalid")
}
}
func TestCredentialReferenceErrorOmitsInput(t *testing.T) {
values := validCredentialReference()
values.Name = "canary-sensitive/input"
_, err := instance.NewCredentialReference(values)
if err == nil {
t.Fatal("invalid reference accepted")
}
if strings.Contains(err.Error(), "canary") {
t.Fatal("error included input")
}
}