diff --git a/tools/cli/internal/cli/sunset/diff.go b/tools/cli/internal/cli/sunset/diff.go index c11eadf157..f9eb49f73a 100644 --- a/tools/cli/internal/cli/sunset/diff.go +++ b/tools/cli/internal/cli/sunset/diff.go @@ -19,6 +19,7 @@ import ( "fmt" "sort" "strings" + "time" "github.com/mongodb/openapi/tools/cli/internal/cli/flag" "github.com/mongodb/openapi/tools/cli/internal/cli/usage" @@ -35,6 +36,10 @@ type DiffOpts struct { specPath string outputPath string format string + from string + to string + toDate *time.Time + fromDate *time.Time } type Diff struct { @@ -68,7 +73,10 @@ func (o *DiffOpts) Run() error { specSunsets := sunset.NewListFromSpec(specInfo) // Find differences - var diffs = findDiffs(baseSunsets, specSunsets, o.basePath, o.specPath) + diffs, err := o.findDiffs(baseSunsets, specSunsets) + if err != nil { + return err + } // Write to output bytes, err := o.newSunsetDiffBytes(diffs) @@ -84,7 +92,7 @@ func (o *DiffOpts) Run() error { return nil } -func findDiffs(baseSunsets, specSunsets []*sunset.Sunset, baseSpecPath, specPath string) []*Diff { +func (o *DiffOpts) findDiffs(baseSunsets, specSunsets []*sunset.Sunset) ([]*Diff, error) { // Create maps for easy lookup baseMap := make(map[string]*sunset.Sunset) for _, s := range baseSunsets { @@ -106,15 +114,14 @@ func findDiffs(baseSunsets, specSunsets []*sunset.Sunset, baseSpecPath, specPath if specSunset, exists := specMap[key]; exists { // Endpoint exists in both specs if baseSunset.SunsetDate != specSunset.SunsetDate { - // Different sunset dates diffs = append(diffs, &Diff{ Operation: baseSunset.Operation, Path: baseSunset.Path, Version: baseSunset.Version, BaseSunsetDate: baseSunset.SunsetDate, SpecSunsetDate: specSunset.SunsetDate, - BaseSpec: baseSpecPath, - Spec: specPath, + BaseSpec: o.basePath, + Spec: o.specPath, Team: baseSunset.Team, }) } @@ -126,8 +133,8 @@ func findDiffs(baseSunsets, specSunsets []*sunset.Sunset, baseSpecPath, specPath Version: baseSunset.Version, BaseSunsetDate: baseSunset.SunsetDate, SpecSunsetDate: "", - BaseSpec: baseSpecPath, - Spec: specPath, + BaseSpec: o.basePath, + Spec: o.specPath, Team: baseSunset.Team, }) } @@ -142,8 +149,8 @@ func findDiffs(baseSunsets, specSunsets []*sunset.Sunset, baseSpecPath, specPath Version: specSunset.Version, BaseSunsetDate: "", SpecSunsetDate: specSunset.SunsetDate, - BaseSpec: baseSpecPath, - Spec: specPath, + BaseSpec: o.basePath, + Spec: o.specPath, Team: specSunset.Team, }) } @@ -156,7 +163,50 @@ func findDiffs(baseSunsets, specSunsets []*sunset.Sunset, baseSpecPath, specPath return iKey < jKey }) - return diffs + // Filter diffs by date range if specified + filteredDiffs, err := o.diffsInRange(diffs) + if err != nil { + return nil, err + } + + return filteredDiffs, nil +} + +func (o *DiffOpts) diffsInRange(diffs []*Diff) ([]*Diff, error) { + var out []*Diff + + if o.from == "" && o.to == "" { + return diffs, nil + } + + for _, d := range diffs { + baseSunsetDate, err := parseSunsetDate(d.BaseSunsetDate) + if err != nil { + return nil, err + } + + specSunsetDate, err := parseSunsetDate(d.SpecSunsetDate) + if err != nil { + return nil, err + } + + if isDateInRange(baseSunsetDate, o.fromDate, o.toDate) || isDateInRange(specSunsetDate, o.fromDate, o.toDate) { + out = append(out, d) + } + } + + return out, nil +} + +func parseSunsetDate(dateStr string) (*time.Time, error) { + if dateStr == "" { + return nil, nil + } + parsedDate, err := time.Parse("2006-01-02", dateStr) + if err != nil { + return nil, err + } + return &parsedDate, err } func makeKey(path, operation, version string) string { @@ -186,6 +236,30 @@ func (o *DiffOpts) newSunsetDiffBytes(diffs []*Diff) ([]byte, error) { return yamlData, nil } +func (o *DiffOpts) validate() error { + if o.from != "" { + value, err := time.Parse("2006-01-02", o.from) + if err != nil { + return err + } + o.fromDate = &value + } + + if o.to != "" { + value, err := time.Parse("2006-01-02", o.to) + if err != nil { + return err + } + o.toDate = &value + } + + if o.from != "" && o.to != "" && o.fromDate.After(*o.toDate) { + return fmt.Errorf("%s date cannot be after %s date", flag.From, flag.To) + } + + return nil +} + // DiffBuilder builds the diff command with the following signature: // sunset diff --base base-spec.json --spec spec.json. func DiffBuilder() *cobra.Command { @@ -197,6 +271,9 @@ func DiffBuilder() *cobra.Command { Use: "diff --base spec1.json --spec spec2.json", Short: "List API endpoints with different sunset dates between two OpenAPI specs.", Args: cobra.NoArgs, + PreRunE: func(_ *cobra.Command, _ []string) error { + return opts.validate() + }, RunE: func(_ *cobra.Command, _ []string) error { return opts.Run() }, @@ -206,6 +283,8 @@ func DiffBuilder() *cobra.Command { cmd.Flags().StringVarP(&opts.specPath, flag.Spec, flag.SpecShort, "", usage.Spec) cmd.Flags().StringVarP(&opts.outputPath, flag.Output, flag.OutputShort, "", usage.Output) cmd.Flags().StringVarP(&opts.format, flag.Format, flag.FormatShort, "json", usage.Format) + cmd.Flags().StringVar(&opts.from, flag.From, "", usage.From) + cmd.Flags().StringVar(&opts.to, flag.To, "", usage.To) _ = cmd.MarkFlagRequired(flag.Base) _ = cmd.MarkFlagRequired(flag.Spec) diff --git a/tools/cli/internal/cli/sunset/diff_test.go b/tools/cli/internal/cli/sunset/diff_test.go index c9b3b6fc50..95b968683a 100644 --- a/tools/cli/internal/cli/sunset/diff_test.go +++ b/tools/cli/internal/cli/sunset/diff_test.go @@ -16,9 +16,11 @@ package sunset import ( "testing" + "time" "github.com/mongodb/openapi/tools/cli/internal/openapi/sunset" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFindDiffsEmpty(t *testing.T) { @@ -32,7 +34,13 @@ func TestFindDiffsEmpty(t *testing.T) { }, } - diff := findDiffs(baseSpecSunsets, baseSpecSunsets, "base.json", "spec.json") + opts := &DiffOpts{ + basePath: "base.json", + specPath: "spec.json", + } + + diff, err := opts.findDiffs(baseSpecSunsets, baseSpecSunsets) + require.NoError(t, err) assert.Empty(t, diff) } @@ -112,7 +120,14 @@ func TestFindDiffsNotEmpty(t *testing.T) { }, } - diff := findDiffs(baseSpecSunsets, specSunsets, "base.json", "spec.json") + opts := &DiffOpts{ + basePath: "base.json", + specPath: "spec.json", + } + + diff, err := opts.findDiffs(baseSpecSunsets, specSunsets) + + require.NoError(t, err) assert.Len(t, diff, 5) assert.Equal(t, "GET", diff[0].Operation) @@ -161,7 +176,376 @@ func TestFindDiffsNotEmpty(t *testing.T) { assert.Equal(t, "APIx", diff[4].Team) } +func TestFindDiffsFiltersByDate(t *testing.T) { + baseSpecSunsets := []*sunset.Sunset{ + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2023-01-01", + SunsetDate: "2025-06-01", + Team: "APIx", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/test", + Version: "2023-01-01", + SunsetDate: "2025-07-01", + Team: "Test", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/groups", + Version: "2023-01-01", + SunsetDate: "2025-06-01", + Team: "Groups", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/groups", + Version: "2023-02-01", + SunsetDate: "2025-07-01", + Team: "Groups", + }, + } + specSunsets := []*sunset.Sunset{ + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2023-01-01", + SunsetDate: "2025-06-02", + Team: "APIx", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/users", + Version: "2023-01-01", + SunsetDate: "2025-06-01", + Team: "Users", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/groups", + Version: "2023-01-01", + SunsetDate: "2025-07-03", + Team: "Groups", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/groups", + Version: "2023-02-01", + SunsetDate: "2025-07-03", + Team: "Groups", + }, + } + + fromDate := time.Date(2025, time.June, 1, 0, 0, 0, 0, time.UTC) + toDate := time.Date(2025, time.June, 15, 0, 0, 0, 0, time.UTC) + + opts := &DiffOpts{ + basePath: "base.json", + specPath: "spec.json", + from: "2025-06-01", + to: "2025-06-15", + fromDate: &fromDate, + toDate: &toDate, + } + + diff, err := opts.findDiffs(baseSpecSunsets, specSunsets) + + require.NoError(t, err) + assert.Len(t, diff, 3) + + assert.Equal(t, "GET", diff[0].Operation) + assert.Equal(t, "/api/atlas/v2/groups", diff[0].Path) + assert.Equal(t, "2023-01-01", diff[0].Version) + assert.Equal(t, "2025-06-01", diff[0].BaseSunsetDate) + assert.Equal(t, "2025-07-03", diff[0].SpecSunsetDate) + assert.Equal(t, "base.json", diff[0].BaseSpec) + assert.Equal(t, "spec.json", diff[0].Spec) + assert.Equal(t, "Groups", diff[0].Team) + + assert.Equal(t, "GET", diff[1].Operation) + assert.Equal(t, "/api/atlas/v2/users", diff[1].Path) + assert.Equal(t, "2023-01-01", diff[1].Version) + assert.Empty(t, diff[1].BaseSunsetDate) + assert.Equal(t, "2025-06-01", diff[1].SpecSunsetDate) + assert.Equal(t, "base.json", diff[1].BaseSpec) + assert.Equal(t, "spec.json", diff[1].Spec) + assert.Equal(t, "Users", diff[1].Team) + + assert.Equal(t, "GET", diff[2].Operation) + assert.Equal(t, "/api/atlas/v2/versions", diff[2].Path) + assert.Equal(t, "2023-01-01", diff[2].Version) + assert.Equal(t, "2025-06-01", diff[2].BaseSunsetDate) + assert.Equal(t, "2025-06-02", diff[2].SpecSunsetDate) + assert.Equal(t, "base.json", diff[2].BaseSpec) + assert.Equal(t, "spec.json", diff[2].Spec) + assert.Equal(t, "APIx", diff[2].Team) +} + func TestMakeKey(t *testing.T) { key := makeKey("/api/atlas/v2/groups", "GET", "2023-01-01") assert.Equal(t, "GET-/api/atlas/v2/groups-2023-01-01", key) } + +func TestDiffsInRangeToAndFrom(t *testing.T) { + fromDate := time.Date(2025, time.June, 3, 0, 0, 0, 0, time.UTC) + toDate := time.Date(2025, time.June, 21, 0, 0, 0, 0, time.UTC) + + opts := &DiffOpts{ + from: "2025-06-03", + to: "2025-06-21", + fromDate: &fromDate, + toDate: &toDate, + } + + diffs := []*Diff{ + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2023-01-01", + BaseSunsetDate: "2025-05-02", + SpecSunsetDate: "2025-05-04", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2024-01-01", + BaseSunsetDate: "2025-06-02", + SpecSunsetDate: "2025-06-04", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2025-01-01", + BaseSunsetDate: "2025-06-10", + SpecSunsetDate: "2025-06-12", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2026-01-01", + BaseSunsetDate: "2025-06-20", + SpecSunsetDate: "2025-06-22", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2027-01-01", + BaseSunsetDate: "2025-07-02", + SpecSunsetDate: "2025-07-04", + }, + } + + result, err := opts.diffsInRange(diffs) + + require.NoError(t, err) + assert.Len(t, result, 3) + + assert.Equal(t, "GET", result[0].Operation) + assert.Equal(t, "/api/atlas/v2/versions", result[0].Path) + assert.Equal(t, "2024-01-01", result[0].Version) + assert.Equal(t, "2025-06-02", result[0].BaseSunsetDate) + assert.Equal(t, "2025-06-04", result[0].SpecSunsetDate) + + assert.Equal(t, "GET", result[1].Operation) + assert.Equal(t, "/api/atlas/v2/versions", result[1].Path) + assert.Equal(t, "2025-01-01", result[1].Version) + assert.Equal(t, "2025-06-10", result[1].BaseSunsetDate) + assert.Equal(t, "2025-06-12", result[1].SpecSunsetDate) + + assert.Equal(t, "GET", result[2].Operation) + assert.Equal(t, "/api/atlas/v2/versions", result[2].Path) + assert.Equal(t, "2026-01-01", result[2].Version) + assert.Equal(t, "2025-06-20", result[2].BaseSunsetDate) + assert.Equal(t, "2025-06-22", result[2].SpecSunsetDate) +} + +func TestDiffsInRangeToAndFromInvalidSunsetDate(t *testing.T) { + fromDate := time.Date(2025, time.June, 3, 0, 0, 0, 0, time.UTC) + toDate := time.Date(2025, time.June, 21, 0, 0, 0, 0, time.UTC) + + opts := &DiffOpts{ + from: "2025-06-03", + to: "2025-06-21", + fromDate: &fromDate, + toDate: &toDate, + } + + diffs := []*Diff{ + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2023-01-01", + BaseSunsetDate: "2025-05", // Invalid date format + SpecSunsetDate: "2025-05-04", + }, + } + + result, err := opts.diffsInRange(diffs) + + require.Error(t, err) + require.Empty(t, result) +} + +func TestDiffsInRangeOnlyTo(t *testing.T) { + toDate := time.Date(2025, time.June, 11, 0, 0, 0, 0, time.UTC) + + opts := &DiffOpts{ + to: "2025-06-11", + toDate: &toDate, + } + + diffs := []*Diff{ + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2023-01-01", + BaseSunsetDate: "", + SpecSunsetDate: "2025-05-04", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2024-01-01", + BaseSunsetDate: "2025-06-02", + SpecSunsetDate: "2025-06-04", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2025-01-01", + BaseSunsetDate: "2025-06-10", + SpecSunsetDate: "2025-06-12", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2026-01-01", + BaseSunsetDate: "2025-06-20", + SpecSunsetDate: "2025-06-22", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2027-01-01", + BaseSunsetDate: "", + SpecSunsetDate: "2025-07-04", + }, + } + + result, err := opts.diffsInRange(diffs) + + require.NoError(t, err) + assert.Len(t, result, 3) + + assert.Equal(t, "GET", result[0].Operation) + assert.Equal(t, "/api/atlas/v2/versions", result[0].Path) + assert.Equal(t, "2023-01-01", result[0].Version) + assert.Empty(t, result[0].BaseSunsetDate) + assert.Equal(t, "2025-05-04", result[0].SpecSunsetDate) + + assert.Equal(t, "GET", result[1].Operation) + assert.Equal(t, "/api/atlas/v2/versions", result[1].Path) + assert.Equal(t, "2024-01-01", result[1].Version) + assert.Equal(t, "2025-06-02", result[1].BaseSunsetDate) + assert.Equal(t, "2025-06-04", result[1].SpecSunsetDate) + + assert.Equal(t, "GET", result[2].Operation) + assert.Equal(t, "/api/atlas/v2/versions", result[2].Path) + assert.Equal(t, "2025-01-01", result[2].Version) + assert.Equal(t, "2025-06-10", result[2].BaseSunsetDate) + assert.Equal(t, "2025-06-12", result[2].SpecSunsetDate) +} + +func TestDiffsInRangeOnlyFrom(t *testing.T) { + fromDate := time.Date(2025, time.June, 11, 0, 0, 0, 0, time.UTC) + + opts := &DiffOpts{ + from: "2025-06-11", + fromDate: &fromDate, + } + + diffs := []*Diff{ + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2023-01-01", + BaseSunsetDate: "2025-05-02", + SpecSunsetDate: "", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2024-01-01", + BaseSunsetDate: "2025-06-02", + SpecSunsetDate: "2025-06-04", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2025-01-01", + BaseSunsetDate: "2025-06-10", + SpecSunsetDate: "2025-06-12", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2026-01-01", + BaseSunsetDate: "2025-06-20", + SpecSunsetDate: "2025-06-22", + }, + { + Operation: "GET", + Path: "/api/atlas/v2/versions", + Version: "2027-01-01", + BaseSunsetDate: "2025-07-02", + SpecSunsetDate: "", + }, + } + + result, err := opts.diffsInRange(diffs) + + require.NoError(t, err) + assert.Len(t, result, 3) + + assert.Equal(t, "GET", result[0].Operation) + assert.Equal(t, "/api/atlas/v2/versions", result[0].Path) + assert.Equal(t, "2025-01-01", result[0].Version) + assert.Equal(t, "2025-06-10", result[0].BaseSunsetDate) + assert.Equal(t, "2025-06-12", result[0].SpecSunsetDate) + + assert.Equal(t, "GET", result[1].Operation) + assert.Equal(t, "/api/atlas/v2/versions", result[1].Path) + assert.Equal(t, "2026-01-01", result[1].Version) + assert.Equal(t, "2025-06-20", result[1].BaseSunsetDate) + assert.Equal(t, "2025-06-22", result[1].SpecSunsetDate) + + assert.Equal(t, "GET", result[2].Operation) + assert.Equal(t, "/api/atlas/v2/versions", result[2].Path) + assert.Equal(t, "2027-01-01", result[2].Version) + assert.Equal(t, "2025-07-02", result[2].BaseSunsetDate) + assert.Empty(t, result[2].SpecSunsetDate) +} + +func TestValidate(t *testing.T) { + opts := &DiffOpts{ + from: "2025-06-01", + to: "2025-06-15", + } + + err := opts.validate() + require.NoError(t, err) + assert.Equal(t, time.Date(2025, time.June, 1, 0, 0, 0, 0, time.UTC), *opts.fromDate) + assert.Equal(t, time.Date(2025, time.June, 15, 0, 0, 0, 0, time.UTC), *opts.toDate) +} + +func TestValidateToIsAfterFrom(t *testing.T) { + opts := &DiffOpts{ + from: "2025-06-15", + to: "2025-06-01", + } + + err := opts.validate() + require.Error(t, err) +} diff --git a/tools/cli/test/e2e/cli/sunset_test.go b/tools/cli/test/e2e/cli/sunset_test.go index d02d2dcdb9..5d5cce6b53 100644 --- a/tools/cli/test/e2e/cli/sunset_test.go +++ b/tools/cli/test/e2e/cli/sunset_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "os" "os/exec" + "path" "testing" "github.com/mongodb/openapi/tools/cli/internal/cli/sunset" @@ -13,9 +14,9 @@ import ( "github.com/stretchr/testify/require" ) -func TestDiff_NoChanges(t *testing.T) { +func TestSunsetDiff_NoChanges(t *testing.T) { baseSpecPath := "../../data/base_spec.json" - outputPath := "diff.json" + outputPath := path.Join(getOutputFolder(t, "sunset"), "diff.json") cliPath := NewBin(t) cmd := exec.CommandContext(context.Background(), cliPath, @@ -43,10 +44,10 @@ func TestDiff_NoChanges(t *testing.T) { assert.Empty(t, results) } -func TestDiff_WithChanges(t *testing.T) { +func TestSunsetDiff_WithChanges(t *testing.T) { baseSpecPath := "../../data/base_spec.json" specPath := "../../data/base_spec_with_mismatching_sunset_dates.json" - outputPath := "diff.json" + outputPath := path.Join(getOutputFolder(t, "sunset"), "diff.json") cliPath := NewBin(t) cmd := exec.CommandContext(context.Background(), cliPath, @@ -104,3 +105,46 @@ func TestDiff_WithChanges(t *testing.T) { assert.Equal(t, baseSpecPath, results[2].BaseSpec) assert.Equal(t, specPath, results[2].Spec) } + +func TestSunsetDiff_WithFilteredChanges(t *testing.T) { + baseSpecPath := "../../data/base_spec.json" + specPath := "../../data/base_spec_with_mismatching_sunset_dates.json" + outputPath := path.Join(getOutputFolder(t, "sunset"), "diff.json") + + cliPath := NewBin(t) + cmd := exec.CommandContext(context.Background(), cliPath, + "sunset", + "diff", + "-b", + baseSpecPath, + "-s", + specPath, + "-o", + outputPath, + "--from", + "2026-02-20", + "--to", + "2026-03-03", + ) + + var o, e bytes.Buffer + cmd.Stdout = &o + cmd.Stderr = &e + require.NoError(t, cmd.Run(), e.String()) + + b, err := os.ReadFile(outputPath) + require.NoError(t, err) + assert.NotEmpty(t, b) + var results []*sunset.Diff + require.NoError(t, json.Unmarshal(b, &results)) + + assert.Len(t, results, 1) + + assert.Equal(t, "GET", results[0].Operation) + assert.Equal(t, "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/search/deployment", results[0].Path) + assert.Equal(t, "2023-01-01", results[0].Version) + assert.Equal(t, "2026-03-01", results[0].BaseSunsetDate) + assert.Empty(t, results[0].SpecSunsetDate) + assert.Equal(t, baseSpecPath, results[0].BaseSpec) + assert.Equal(t, specPath, results[0].Spec) +}