206 lines
7.8 KiB
Go
206 lines
7.8 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// 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 helpers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageDriveFolderContractsPublishResultAndDryRun(t *testing.T) {
|
|
drive := newDriveCommand()
|
|
want := map[string]struct {
|
|
canonical string
|
|
dryRun bool
|
|
outcomes []contract.ResultOutcome
|
|
properties []string
|
|
}{
|
|
"status": {
|
|
canonical: "drive.folder_status",
|
|
outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomeFailure},
|
|
properties: []string{"detection", "modified", "new_local", "new_remote", "unchanged", "unknown"},
|
|
},
|
|
"pull": {
|
|
canonical: "drive.folder_pull",
|
|
dryRun: true,
|
|
outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomePartialFailure, contract.ResultOutcomeFailure},
|
|
properties: []string{"dry_run", "executed", "if_exists", "items", "operation", "plan", "preview_kind", "summary"},
|
|
},
|
|
"push": {
|
|
canonical: "drive.folder_push",
|
|
dryRun: true,
|
|
outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomePartialFailure, contract.ResultOutcomeFailure},
|
|
properties: []string{"dry_run", "executed", "if_exists", "items", "operation", "plan", "preview_kind", "summary"},
|
|
},
|
|
"sync": {
|
|
canonical: "drive.folder_sync",
|
|
dryRun: true,
|
|
outcomes: []contract.ResultOutcome{contract.ResultOutcomeSuccess, contract.ResultOutcomePartialFailure, contract.ResultOutcomeFailure},
|
|
properties: []string{"detection", "diff", "dry_run", "executed", "items", "operation", "plan", "preview_kind", "summary"},
|
|
},
|
|
}
|
|
|
|
for name, expectation := range want {
|
|
t.Run(name, func(t *testing.T) {
|
|
leaf, _, err := drive.Find([]string{name})
|
|
if err != nil || leaf == nil {
|
|
t.Fatalf("find drive %s: command=%v err=%v", name, leaf, err)
|
|
}
|
|
final, ok := contractfinal.RuntimeContractFinal(leaf)
|
|
if !ok || final.Identity == nil || final.Identity.CanonicalPath != expectation.canonical {
|
|
t.Fatalf("drive %s ContractFinal identity = %#v", name, final.Identity)
|
|
}
|
|
if final.Result == nil || !reflect.DeepEqual(final.Result.Outcomes, expectation.outcomes) {
|
|
t.Fatalf("drive %s result = %#v, want outcomes %#v", name, final.Result, expectation.outcomes)
|
|
}
|
|
properties := resultSchemaProperties(t, final.Result.DataSchema)
|
|
if got := sortedContractSchemaKeys(properties); !reflect.DeepEqual(got, expectation.properties) {
|
|
t.Fatalf("drive %s result properties = %#v, want %#v", name, got, expectation.properties)
|
|
}
|
|
if expectation.dryRun {
|
|
if final.DryRun == nil || final.DryRun.PreviewKind != contract.DryRunPreviewPlan || !final.DryRun.RemoteReads {
|
|
t.Fatalf("drive %s dry_run = %#v, want remote-reading plan", name, final.DryRun)
|
|
}
|
|
if final.Selection == nil || len(final.Selection.ExampleDispositions) != len(final.Selection.Examples) {
|
|
t.Fatalf("drive %s stateful example dispositions = %#v", name, final.Selection)
|
|
}
|
|
for _, disposition := range final.Selection.ExampleDispositions {
|
|
if disposition.Mode != contract.ExampleDispositionModeContractOnly ||
|
|
disposition.ReasonCode != contract.ExampleDispositionReasonStatefulPreflight || !disposition.Reviewed {
|
|
t.Fatalf("drive %s invalid example disposition: %#v", name, disposition)
|
|
}
|
|
}
|
|
} else if final.DryRun != nil {
|
|
t.Fatalf("drive %s unexpectedly publishes dry_run: %#v", name, final.DryRun)
|
|
}
|
|
|
|
if name == "sync" {
|
|
assertDriveSyncResultSchema(t, final.Result.DataSchema)
|
|
}
|
|
if name == "push" {
|
|
assertDrivePushResultSchema(t, final.Result.DataSchema)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertDrivePushResultSchema(t *testing.T, raw json.RawMessage) {
|
|
t.Helper()
|
|
properties := resultSchemaProperties(t, raw)
|
|
plan := schemaObjectProperty(t, properties, "plan")
|
|
summary := schemaObjectProperty(t, schemaProperties(t, plan), "summary")
|
|
summaryProperties := schemaProperties(t, summary)
|
|
for _, name := range []string{"planned_uploads", "planned_skips", "planned_folders"} {
|
|
if _, ok := summaryProperties[name]; !ok {
|
|
t.Fatalf("push plan summary schema is missing %s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func resultSchemaProperties(t *testing.T, raw json.RawMessage) map[string]any {
|
|
t.Helper()
|
|
var schema map[string]any
|
|
if err := json.Unmarshal(raw, &schema); err != nil {
|
|
t.Fatalf("result data_schema is not JSON: %v\n%s", err, raw)
|
|
}
|
|
properties, ok := schema["properties"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("result data_schema.properties = %#v, want object", schema["properties"])
|
|
}
|
|
return properties
|
|
}
|
|
|
|
func sortedContractSchemaKeys(values map[string]any) []string {
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func assertDriveSyncResultSchema(t *testing.T, raw json.RawMessage) {
|
|
t.Helper()
|
|
properties := resultSchemaProperties(t, raw)
|
|
diff := schemaObjectProperty(t, properties, "diff")
|
|
assertSchemaRequired(t, diff, "new_local", "new_remote", "modified", "unchanged", "unknown")
|
|
if got := sortedContractSchemaKeys(schemaProperties(t, diff)); !reflect.DeepEqual(got, []string{"modified", "new_local", "new_remote", "unchanged", "unknown"}) {
|
|
t.Fatalf("sync diff properties = %#v", got)
|
|
}
|
|
|
|
plan := schemaObjectProperty(t, properties, "plan")
|
|
assertSchemaRequired(t, plan, "detection", "diff", "summary", "items")
|
|
planProperties := schemaProperties(t, plan)
|
|
planDiff := schemaObjectProperty(t, planProperties, "diff")
|
|
assertSchemaRequired(t, planDiff, "new_local", "new_remote", "modified", "unchanged", "unknown")
|
|
planSummary := schemaObjectProperty(t, planProperties, "summary")
|
|
planSummaryProperties := schemaProperties(t, planSummary)
|
|
for _, name := range []string{"planned_pulls", "planned_pushes", "planned_skips", "planned_folders"} {
|
|
if _, ok := planSummaryProperties[name]; !ok {
|
|
t.Fatalf("sync plan summary schema is missing %s", name)
|
|
}
|
|
}
|
|
|
|
var schema map[string]any
|
|
if err := json.Unmarshal(raw, &schema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oneOf, ok := schema["oneOf"].([]any)
|
|
if !ok || len(oneOf) != 2 {
|
|
t.Fatalf("sync result oneOf = %#v, want execution/dry-run alternatives", schema["oneOf"])
|
|
}
|
|
}
|
|
|
|
func schemaObjectProperty(t *testing.T, properties map[string]any, name string) map[string]any {
|
|
t.Helper()
|
|
property, ok := properties[name].(map[string]any)
|
|
if !ok || property["type"] != "object" {
|
|
t.Fatalf("schema property %s = %#v, want object", name, properties[name])
|
|
}
|
|
return property
|
|
}
|
|
|
|
func schemaProperties(t *testing.T, schema map[string]any) map[string]any {
|
|
t.Helper()
|
|
properties, ok := schema["properties"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema properties = %#v, want object", schema["properties"])
|
|
}
|
|
return properties
|
|
}
|
|
|
|
func assertSchemaRequired(t *testing.T, schema map[string]any, want ...string) {
|
|
t.Helper()
|
|
raw, ok := schema["required"].([]any)
|
|
if !ok {
|
|
t.Fatalf("schema required = %#v", schema["required"])
|
|
}
|
|
got := make([]string, 0, len(raw))
|
|
for _, value := range raw {
|
|
text, ok := value.(string)
|
|
if !ok {
|
|
t.Fatalf("schema required value = %#v, want string", value)
|
|
}
|
|
got = append(got, text)
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("schema required = %#v, want %#v", got, want)
|
|
}
|
|
}
|