Files
dingtalk-workspace-cli/internal/helpers/drive_folder_contract_test.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)
}
}