307 lines
11 KiB
Go
307 lines
11 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
package interfacesnapshot
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type flagMigrationErrorReader struct{}
|
|
|
|
func (flagMigrationErrorReader) Read([]byte) (int, error) {
|
|
return 0, errors.New("injected read failure")
|
|
}
|
|
|
|
const validFlagMigrationManifestJSON = `{
|
|
"version": 1,
|
|
"migrations": [
|
|
{
|
|
"command": "dws chat message recall",
|
|
"legacy": {
|
|
"name": "msg-id",
|
|
"before": {"present": true, "type": "string", "required": true, "scope": "local"},
|
|
"after": {"present": true, "type": "string", "hidden": true, "scope": "local", "alias_of": "message-id"}
|
|
},
|
|
"canonical": {
|
|
"name": "message-id",
|
|
"before": {"present": false},
|
|
"after": {"present": true, "type": "string", "required": true, "scope": "local"}
|
|
},
|
|
"state": "pending",
|
|
"reason": "Reviewed message identifier flag migration."
|
|
}
|
|
]
|
|
}`
|
|
|
|
func optionalFlagMigrationManifestJSON() string {
|
|
manifest := strings.Replace(
|
|
validFlagMigrationManifestJSON,
|
|
`"before": {"present": true, "type": "string", "required": true, "scope": "local"}`,
|
|
`"before": {"present": true, "type": "string", "scope": "local"}`,
|
|
1,
|
|
)
|
|
return strings.Replace(
|
|
manifest,
|
|
`"after": {"present": true, "type": "string", "required": true, "scope": "local"}`,
|
|
`"after": {"present": true, "type": "string", "scope": "local"}`,
|
|
1,
|
|
)
|
|
}
|
|
|
|
func hiddenCanonicalFlagMigrationManifestJSON() string {
|
|
return strings.Replace(
|
|
validFlagMigrationManifestJSON,
|
|
`"before": {"present": false}`,
|
|
`"before": {"present": true, "type": "string", "hidden": true, "scope": "local"}`,
|
|
1,
|
|
)
|
|
}
|
|
|
|
func TestApprovedFlagMigrationManifestRemainsValid(t *testing.T) {
|
|
manifest, err := os.Open("../../scripts/policy/interface-migrations/approved-flag-migrations-v1.json")
|
|
if err != nil {
|
|
t.Fatalf("open approved flag migration manifest: %v", err)
|
|
}
|
|
defer manifest.Close()
|
|
|
|
if _, err := ReadFlagMigrationManifest(manifest); err != nil {
|
|
t.Fatalf("approved flag migration manifest is invalid: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsUnknownFields(t *testing.T) {
|
|
_, err := ReadFlagMigrationManifest(strings.NewReader(`{
|
|
"version": 1,
|
|
"migrations": [],
|
|
"allow_all_chat_flags": true
|
|
}`))
|
|
if err == nil || !strings.Contains(err.Error(), "unknown field") {
|
|
t.Fatalf("ReadFlagMigrationManifest() error = %v, want unknown field rejection", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsDuplicateFields(t *testing.T) {
|
|
for _, input := range []string{
|
|
`{"version": 999, "version": 1, "migrations": []}`,
|
|
strings.Replace(validFlagMigrationManifestJSON, `"state": "pending"`, `"state": "consumed", "state": "pending"`, 1),
|
|
} {
|
|
_, err := ReadFlagMigrationManifest(strings.NewReader(input))
|
|
if err == nil || !strings.Contains(err.Error(), "duplicate field") {
|
|
t.Fatalf("ReadFlagMigrationManifest() error = %v, want duplicate field rejection", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsNonCanonicalFields(t *testing.T) {
|
|
for _, input := range []string{
|
|
`{"Version": 1, "migrations": []}`,
|
|
`{"version": 1, "Version": 1, "migrations": []}`,
|
|
strings.Replace(validFlagMigrationManifestJSON, `"state": "pending"`, `"State": "pending"`, 1),
|
|
} {
|
|
_, err := ReadFlagMigrationManifest(strings.NewReader(input))
|
|
if err == nil || !strings.Contains(err.Error(), "non-canonical field") {
|
|
t.Fatalf("ReadFlagMigrationManifest() error = %v, want non-canonical field rejection", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsNullScalars(t *testing.T) {
|
|
for _, input := range []string{
|
|
strings.Replace(validFlagMigrationManifestJSON, `"required": true`, `"required": null`, 1),
|
|
strings.Replace(validFlagMigrationManifestJSON, `"scope": "local"`, `"scope": "local", "shorthand": null`, 1),
|
|
`{"version": null, "migrations": []}`,
|
|
} {
|
|
_, err := ReadFlagMigrationManifest(strings.NewReader(input))
|
|
if err == nil || !strings.Contains(err.Error(), "must be") || !strings.Contains(err.Error(), "null") {
|
|
t.Fatalf("ReadFlagMigrationManifest() error = %v, want scalar null rejection", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsWrongJSONTypes(t *testing.T) {
|
|
for _, input := range []string{
|
|
strings.Replace(validFlagMigrationManifestJSON, `"required": true`, `"required": "true"`, 1),
|
|
strings.Replace(validFlagMigrationManifestJSON, `"scope": "local"`, `"scope": false`, 1),
|
|
`{"version": "1", "migrations": []}`,
|
|
`{"version": 1, "migrations": {}}`,
|
|
} {
|
|
if _, err := ReadFlagMigrationManifest(strings.NewReader(input)); err == nil {
|
|
t.Fatal("ReadFlagMigrationManifest() accepted a field with the wrong JSON type")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageReadFlagMigrationManifestReportsReaderFailure(t *testing.T) {
|
|
_, err := ReadFlagMigrationManifest(flagMigrationErrorReader{})
|
|
if err == nil || !strings.Contains(err.Error(), "injected read failure") {
|
|
t.Fatalf("ReadFlagMigrationManifest() error = %v, want reader failure", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageReadFlagMigrationManifestValidatesExactEntries(t *testing.T) {
|
|
if _, err := ReadFlagMigrationManifest(strings.NewReader(validFlagMigrationManifestJSON)); err != nil {
|
|
t.Fatalf("ReadFlagMigrationManifest(valid) error = %v", err)
|
|
}
|
|
if _, err := ReadFlagMigrationManifest(strings.NewReader(optionalFlagMigrationManifestJSON())); err != nil {
|
|
t.Fatalf("ReadFlagMigrationManifest(optional rename) error = %v", err)
|
|
}
|
|
if _, err := ReadFlagMigrationManifest(strings.NewReader(hiddenCanonicalFlagMigrationManifestJSON())); err != nil {
|
|
t.Fatalf("ReadFlagMigrationManifest(hidden canonical promotion) error = %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "unsupported version",
|
|
input: strings.Replace(validFlagMigrationManifestJSON, `"version": 1`, `"version": 2`, 1),
|
|
wantErr: "unsupported flag migration manifest version",
|
|
},
|
|
{
|
|
name: "missing migrations array",
|
|
input: `{"version": 1}`,
|
|
wantErr: "migrations must be an array",
|
|
},
|
|
{
|
|
name: "null migrations array",
|
|
input: `{"version": 1, "migrations": null}`,
|
|
wantErr: "migrations must be an array",
|
|
},
|
|
{
|
|
name: "wildcard command",
|
|
input: strings.Replace(validFlagMigrationManifestJSON, "dws chat message recall", "dws chat *", 1),
|
|
wantErr: "exact command path",
|
|
},
|
|
{
|
|
name: "wildcard flag",
|
|
input: strings.Replace(validFlagMigrationManifestJSON, `"name": "msg-id"`, `"name": "msg-*"`, 1),
|
|
wantErr: "exact legacy flag",
|
|
},
|
|
{
|
|
name: "empty reason",
|
|
input: strings.Replace(validFlagMigrationManifestJSON, "Reviewed message identifier flag migration.", "", 1),
|
|
wantErr: "non-empty reason",
|
|
},
|
|
{
|
|
name: "invalid state",
|
|
input: strings.Replace(validFlagMigrationManifestJSON, `"state": "pending"`, `"state": "approved"`, 1),
|
|
wantErr: "invalid state",
|
|
},
|
|
{
|
|
name: "legacy removed",
|
|
input: strings.Replace(validFlagMigrationManifestJSON, `"after": {"present": true, "type": "string", "hidden": true, "scope": "local", "alias_of": "message-id"}`, `"after": {"present": false}`, 1),
|
|
wantErr: "legacy flag must remain present",
|
|
},
|
|
{
|
|
name: "canonical hidden",
|
|
input: strings.Replace(validFlagMigrationManifestJSON, `"after": {"present": true, "type": "string", "required": true, "scope": "local"}`, `"after": {"present": true, "type": "string", "required": true, "hidden": true, "scope": "local"}`, 1),
|
|
wantErr: "canonical flag must remain visible",
|
|
},
|
|
{
|
|
name: "required legacy becomes optional canonical",
|
|
input: strings.Replace(
|
|
validFlagMigrationManifestJSON,
|
|
`"after": {"present": true, "type": "string", "required": true, "scope": "local"}`,
|
|
`"after": {"present": true, "type": "string", "scope": "local"}`,
|
|
1,
|
|
),
|
|
wantErr: "requiredness must be preserved from legacy before to canonical after",
|
|
},
|
|
{
|
|
name: "optional legacy becomes required canonical",
|
|
input: strings.Replace(
|
|
optionalFlagMigrationManifestJSON(),
|
|
`"after": {"present": true, "type": "string", "scope": "local"}`,
|
|
`"after": {"present": true, "type": "string", "required": true, "scope": "local"}`,
|
|
1,
|
|
),
|
|
wantErr: "requiredness must be preserved from legacy before to canonical after",
|
|
},
|
|
{
|
|
name: "existing canonical changes requiredness",
|
|
input: strings.Replace(
|
|
validFlagMigrationManifestJSON,
|
|
`"before": {"present": false}`,
|
|
`"before": {"present": true, "type": "string", "scope": "local"}`,
|
|
1,
|
|
),
|
|
wantErr: "canonical flag requiredness must remain unchanged when already present",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := ReadFlagMigrationManifest(strings.NewReader(test.input))
|
|
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
|
t.Fatalf("ReadFlagMigrationManifest() error = %v, want %q", err, test.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageFlagMigrationManifestRejectsDuplicateAndInexactContracts(t *testing.T) {
|
|
manifest, err := ReadFlagMigrationManifest(strings.NewReader(validFlagMigrationManifestJSON))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
duplicate := manifest
|
|
duplicate.Migrations = append(duplicate.Migrations, duplicate.Migrations[0])
|
|
if err := duplicate.Validate(); err == nil || !strings.Contains(err.Error(), "duplicates") {
|
|
t.Fatalf("duplicate migration error = %v", err)
|
|
}
|
|
|
|
whitespaceReason := manifest
|
|
whitespaceReason.Migrations = append([]FlagMigration(nil), manifest.Migrations...)
|
|
whitespaceReason.Migrations[0].Reason = " reviewed "
|
|
if err := whitespaceReason.Validate(); err == nil || !strings.Contains(err.Error(), "trimmed") {
|
|
t.Fatalf("whitespace reason error = %v", err)
|
|
}
|
|
|
|
canonicalDrift := manifest
|
|
canonicalDrift.Migrations = append([]FlagMigration(nil), manifest.Migrations...)
|
|
canonicalDrift.Migrations[0].Canonical.Before = FlagMigrationState{
|
|
Present: true,
|
|
Type: "string",
|
|
Required: true,
|
|
Scope: "local",
|
|
}
|
|
canonicalDrift.Migrations[0].Canonical.After.Type = "stringSlice"
|
|
if err := canonicalDrift.Validate(); err == nil || !strings.Contains(err.Error(), "canonical flag type") {
|
|
t.Fatalf("canonical type drift error = %v", err)
|
|
}
|
|
|
|
_, err = ReadFlagMigrationManifest(strings.NewReader(validFlagMigrationManifestJSON + ` {}`))
|
|
if err == nil || !strings.Contains(err.Error(), "trailing") {
|
|
t.Fatalf("trailing JSON error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageAuthorizeFlagMigrationsReturnsExactBaseOwnedApproval(t *testing.T) {
|
|
pending := coverageManifest(FlagMigrationPending)
|
|
consumed := coverageManifest(FlagMigrationConsumed)
|
|
migration := pending.Migrations[0]
|
|
before := coverageMigrationSnapshot(migration, false, false)
|
|
after := coverageMigrationSnapshot(migration, true, false)
|
|
|
|
got, err := AuthorizeFlagMigrations(
|
|
after,
|
|
map[string]Snapshot{"merge-base": before, "stable": before},
|
|
pending,
|
|
consumed,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("AuthorizeFlagMigrations() error = %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, pending.Migrations) {
|
|
t.Fatalf("AuthorizeFlagMigrations() = %#v, want exact base-owned approval %#v", got, pending.Migrations)
|
|
}
|
|
}
|