Files
gitea-tea/internal/app/pull_integration_test.go
T
bircni c5cf6b9ed9 feat(pr): review with -a/-r/-c, a review create command and the current branch's PR
tea pr review takes gh's review flags: -a/--approve, -r/--request-changes
and -c/--comment, with -b/--body, -F/--body-file and -e/--editor, and
--pending for a review to submit later. --event is hidden but still
accepted, case-insensitively, as approve, approved, request-changes,
request_changes, comment or pending. tea pr review create is the same
command under the name people look for.

The pull request argument is optional and accepts #N, URLs and branch
names; without one, the pull request of the current branch is reviewed.
Without a review type, tea opens the interactive review on a terminal
and otherwise fails with "choose a review type" instead of posting a
comment. Review types given as a subcommand ("tea pr review approve 13")
point at the flag, and approving or requesting changes on one's own pull
request fails before any editor opens.

The interactive review asks for the type first, hiding Approve and
Request changes for the author, then for an inline body that ctrl+e
opens in the configured editor, then offers to annotate the diff, and
confirms with a default of yes. A declined submission keeps the body as
a draft.

Successful reviews report "✓ Approved pull request alice/x#13 (title)"
on a terminal and print the review URL. review submit finds the
caller's pending review when no ID is given, review list and comments
are tables with headers, and dismiss, undismiss, delete, request and
cancel-request say what they did. Every review command has examples and
completes pull request and review IDs.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-11 23:54:45 +02:00

356 lines
20 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"gitea.dev/tea/internal/clierr"
"gitea.dev/tea/internal/git"
"gitea.dev/tea/internal/services"
"github.com/stretchr/testify/require"
)
func TestGiteaPullEditLifecycle(t *testing.T) {
gitea, deps, out, diagnostics := giteaDeps(t)
execute := outputExecutor(t, &deps, out, diagnostics)
out.Reset()
diagnostics.Reset()
identityErr := Execute(context.Background(), deps, []string{"issue", "create", "-R", "integration/nonexistent", "--title", "Must not submit", "--body", "Body", "--user", "tea-deliberately-wrong-account", "--no-input"})
require.ErrorContains(t, identityErr, "does not belong to the selected account")
require.Equal(t, clierr.Authentication, clierr.ExitCode(identityErr))
require.Empty(t, out.String())
require.Empty(t, diagnostics.String())
var repo services.Repository
name := fmt.Sprintf("tea-pr-edit-%d", time.Now().UnixNano())
require.NoError(t, json.Unmarshal(execute("repo", "create", name, "--auto-init", "--private", "--json"), &repo))
t.Cleanup(func() {
if err := Execute(context.Background(), deps, []string{"repo", "delete", "-R", repo.FullName, "--yes"}); err != nil {
t.Errorf("repository cleanup: %v", err)
}
})
execute("branch", "create", "feature", "-R", repo.FullName)
deps.Streams.In = strings.NewReader("{\"branch\":\"feature\",\"content\":\"dGVzdAo=\",\"message\":\"Disposable PR change\"}")
execute("api", "repos/"+repo.FullName+"/contents/change.txt", "--input", "-")
var label services.Label
require.NoError(t, json.Unmarshal(execute("label", "create", "--name", "review", "--color", "00ff00", "-R", repo.FullName, "--json"), &label))
exportPath := filepath.Join(t.TempDir(), "labels.txt")
execute("label", "export", "-R", repo.FullName, "--output-file", exportPath)
exported, err := os.ReadFile(exportPath)
require.NoError(t, err)
require.Contains(t, string(exported), "00ff00 review ;")
jsonExportPath := exportPath + ".json"
execute("label", "export", "-R", repo.FullName, "--output-file", jsonExportPath, "--json")
jsonExport, err := os.ReadFile(jsonExportPath)
require.NoError(t, err)
var exportedLabels []services.Label
require.NoError(t, json.Unmarshal(jsonExport, &exportedLabels))
require.Contains(t, exportedLabels, label)
var milestone services.Milestone
require.NoError(t, json.Unmarshal(execute("milestone", "create", "--title", "Disposable", "-R", repo.FullName, "--json"), &milestone))
var dated services.Milestone
require.NoError(t, json.Unmarshal(execute("milestone", "edit", fmt.Sprint(milestone.ID), "-R", repo.FullName, "--deadline", "2028-01-15", "--json"), &dated))
require.NotNil(t, dated.Deadline)
preserved := *dated.Deadline
for _, verb := range []string{"view", "close", "reopen"} {
var selected services.Milestone
require.NoError(t, json.Unmarshal(execute("milestone", verb, milestone.Title, "--by-name", "-R", repo.FullName, "--json"), &selected))
require.Equal(t, milestone.ID, selected.ID)
require.NotNil(t, selected.Deadline)
require.Equal(t, preserved, *selected.Deadline)
if verb == "close" {
require.Equal(t, "closed", selected.State)
} else {
require.Equal(t, "open", selected.State)
}
}
require.NoError(t, json.Unmarshal(execute("milestone", "edit", fmt.Sprint(milestone.ID), "-R", repo.FullName, "--deadline=", "--json"), &dated))
require.Nil(t, dated.Deadline)
var secondMilestone services.Milestone
require.NoError(t, json.Unmarshal(execute("milestone", "create", "--title", "Bulk milestone", "-R", repo.FullName, "--json"), &secondMilestone))
for _, verb := range []string{"close", "reopen"} {
var changed []services.Milestone
require.NoError(t, json.Unmarshal(execute("milestone", verb, milestone.Title, secondMilestone.Title, "--by-name", "-R", repo.FullName, "--json"), &changed))
require.Len(t, changed, 2)
require.Equal(t, milestone.ID, changed[0].ID)
require.Equal(t, secondMilestone.ID, changed[1].ID)
for _, item := range changed {
if verb == "close" {
require.Equal(t, "closed", item.State)
} else {
require.Equal(t, "open", item.State)
}
}
}
var pull services.Pull
require.NoError(t, json.Unmarshal(executeWithEditorIntegration(t, deps, "", "Editor PR body", "pr", "create", "-R", repo.FullName, "--title", "[wip] Initial", "--head", "feature", "--base", repo.DefaultBranch, "--deadline", "2028-01-15", "--assignee", strings.SplitN(repo.FullName, "/", 2)[0], "--label", "review", "--milestone-name", milestone.Title), &pull))
require.Equal(t, []services.Label{label}, pull.Labels)
require.Equal(t, []string{strings.SplitN(repo.FullName, "/", 2)[0]}, pull.Assignees)
require.Equal(t, &services.IssueMilestone{ID: milestone.ID, Title: milestone.Title}, pull.Milestone)
require.NotNil(t, pull.Deadline)
require.Equal(t, "2028-01-15T00:00:00Z", *pull.Deadline)
n := fmt.Sprint(pull.Number)
require.Equal(t, "[]\n", string(execute("pr", "attachment", "list", n, "-R", repo.FullName, "--json")))
verifyResourceBrowser(t, deps, pull.URL, "pr", "view", n, "--repo", repo.FullName)
verifyBodyEditorIntegration(t, deps, "pr", repo.FullName, n, pull.Body)
verifyIssueAttachmentLifecycle(t, deps, "pr", repo.FullName, n)
var pullStatus services.PullStatus
require.NoError(t, json.Unmarshal(execute("pr", "status", "-R", repo.FullName, "--json"), &pullStatus))
require.Len(t, pullStatus.Authored, 1)
require.Equal(t, pull.Number, pullStatus.Authored[0].Number)
require.Equal(t, repo.FullName, pullStatus.Authored[0].Repository)
require.Len(t, pullStatus.Assigned, 1)
require.Equal(t, pull.Number, pullStatus.Assigned[0].Number)
require.Equal(t, repo.FullName, pullStatus.Assigned[0].Repository)
require.Empty(t, pullStatus.ReviewRequested)
var filtered []services.Pull
require.NoError(t, json.Unmarshal(execute("pr", "list", "-R", repo.FullName, "--author", strings.SplitN(repo.FullName, "/", 2)[0], "--base", repo.DefaultBranch, "--sort", "oldest", "--milestone", fmt.Sprint(milestone.ID), "--label", "review", "--json"), &filtered))
require.Len(t, filtered, 1)
require.Equal(t, pull.Number, filtered[0].Number)
require.Equal(t, strings.SplitN(repo.FullName, "/", 2)[0], filtered[0].Author)
require.NotEmpty(t, filtered[0].UpdatedAt)
require.NoError(t, json.Unmarshal(execute("pr", "list", "-R", repo.FullName, "--base", "tea-nonexistent-target-branch", "--json"), &filtered))
require.Empty(t, filtered)
var head struct {
Head struct {
SHA string `json:"sha"`
} `json:"head"`
}
require.NoError(t, json.Unmarshal(execute("api", "repos/"+repo.FullName+"/pulls/"+n), &head))
require.NotEmpty(t, head.Head.SHA)
execute("repo", "edit", "-R", repo.FullName, "--private=false")
workingTree := filepath.Join(t.TempDir(), "checkout")
runner := git.System{In: deps.Streams.In, ErrOut: diagnostics}
out.Reset()
diagnostics.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "clone", repo.FullName, workingTree, "--depth", "1", "--no-input", "--json"}))
var cloned services.Repository
require.NoError(t, json.Unmarshal(out.Bytes(), &cloned))
require.Equal(t, repo.ID, cloned.ID)
require.Equal(t, repo.FullName, cloned.FullName)
shallow, err := runner.Output(context.Background(), workingTree, false, "rev-parse", "--is-shallow-repository")
require.NoError(t, err)
require.Equal(t, "true", shallow)
var checkout git.CheckoutResult
out.Reset()
diagnostics.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "checkout", n, "-R", repo.FullName, "--cwd", workingTree, "--no-input", "--json"}))
require.NoError(t, json.Unmarshal(out.Bytes(), &checkout))
require.Contains(t, diagnostics.String(), "FETCH_HEAD")
require.Equal(t, "pr/"+n, checkout.Branch)
require.Equal(t, head.Head.SHA, checkout.Commit)
actual, err := runner.Output(context.Background(), workingTree, false, "rev-parse", "HEAD")
require.NoError(t, err)
require.Equal(t, head.Head.SHA, actual)
execute("repo", "edit", "-R", repo.FullName, "--private=true")
for _, state := range []string{"failure", "success"} {
payload, err := json.Marshal(map[string]string{"state": state, "context": "tea-check", "description": "Disposable check"})
require.NoError(t, err)
deps.Streams.In = bytes.NewReader(payload)
execute("api", "repos/"+repo.FullName+"/statuses/"+head.Head.SHA, "--input", "-")
}
var checks []services.Check
require.NoError(t, json.Unmarshal(execute("pr", "checks", n, "-R", repo.FullName, "--json"), &checks))
require.Len(t, checks, 1)
require.Equal(t, "success", checks[0].State)
require.Equal(t, "tea-check", checks[0].Name)
require.NoError(t, json.Unmarshal(execute("pr", "edit", n, "-R", repo.FullName, "--set-labels", "repo:review", "--json"), &pull))
require.Equal(t, []services.Label{label}, pull.Labels)
require.NoError(t, json.Unmarshal(execute("pr", "edit", n, "-R", repo.FullName, "--set-labels=", "--json"), &pull))
require.Empty(t, pull.Labels)
require.NotNil(t, pull.Labels)
require.NoError(t, json.Unmarshal(execute("pr", "edit", n, "-R", repo.FullName, "--ready", "--body", "Updated", "--milestone-name", milestone.Title, "--allow-maintainer-edit=false", "--json"), &pull))
require.Equal(t, "Initial", pull.Title)
require.Equal(t, "Updated", pull.Body)
execute("pr", "edit", n, "-R", repo.FullName, "--milestone=0", "--body=", "--set-assignees=", "--draft", "--deadline=")
var raw struct {
Milestone *struct{ ID int64 }
Assignees []any
Title, Body string
Deadline *string `json:"due_date"`
}
require.NoError(t, json.Unmarshal(execute("api", "repos/"+repo.FullName+"/pulls/"+n), &raw))
require.Nil(t, raw.Milestone)
require.Nil(t, raw.Deadline)
require.Empty(t, raw.Assignees)
require.Empty(t, raw.Body)
require.Equal(t, "WIP: Initial", raw.Title)
require.NoError(t, json.Unmarshal(execute("pr", "edit", n, "-R", repo.FullName, "--deadline", "2028-02-01T00:30:00+02:00", "--json"), &pull))
assertEditedDeadline(t, pull.Deadline, "2028-02-01")
require.NoError(t, json.Unmarshal(execute("pr", "edit", n, "-R", repo.FullName, "--deadline=", "--json"), &pull))
require.Nil(t, pull.Deadline)
var issue services.Issue
owner, _, _ := strings.Cut(repo.FullName, "/")
require.NoError(t, json.Unmarshal(execute("issue", "create", "-R", repo.FullName, "--title", "Deadline fixture", "--deadline", "2028-01-15", "--ref", "feature", "--assignee", owner, "--milestone-name", milestone.Title, "--json"), &issue))
require.NotNil(t, issue.Deadline)
require.Equal(t, "feature", issue.Ref)
require.Equal(t, []string{owner}, issue.Assignees)
require.NotNil(t, issue.Milestone)
require.Equal(t, milestone.ID, issue.Milestone.ID)
issueNumber := fmt.Sprint(issue.Number)
require.NoError(t, json.Unmarshal(execute("issue", "edit", issueNumber, "-R", repo.FullName, "--ref", repo.DefaultBranch, "--json"), &issue))
require.Equal(t, repo.DefaultBranch, issue.Ref)
require.NoError(t, json.Unmarshal(execute("issue", "edit", issueNumber, "-R", repo.FullName, "--ref=", "--set-assignees=", "--milestone=0", "--json"), &issue))
require.Empty(t, issue.Ref)
require.Empty(t, issue.Assignees)
require.Nil(t, issue.Milestone)
require.NoError(t, json.Unmarshal(execute("issue", "edit", issueNumber, "-R", repo.FullName, "--deadline", "2028-02-01T00:30:00+02:00", "--json"), &issue))
assertEditedDeadline(t, issue.Deadline, "2028-02-01")
require.NoError(t, json.Unmarshal(execute("issue", "edit", issueNumber, "-R", repo.FullName, "--deadline=", "--json"), &issue))
require.Nil(t, issue.Deadline)
commentsFile := filepath.Join(t.TempDir(), "comments.json")
require.NoError(t, os.WriteFile(commentsFile, []byte(`[{"path":"change.txt","body":"Disposable inline review","new_position":1}]`), 0o600))
var review services.Review
require.NoError(t, json.Unmarshal(execute("pr", "review", n, "-R", repo.FullName, "--comment", "--comments-file", commentsFile, "--json"), &review))
require.NotZero(t, review.ID)
require.NotEmpty(t, review.Author)
require.Equal(t, "COMMENT", review.State)
var reviews []services.Review
require.NoError(t, json.Unmarshal(execute("pr", "review", "list", n, "-R", repo.FullName, "--json"), &reviews))
require.Len(t, reviews, 1)
var comments []services.ReviewComment
require.NoError(t, json.Unmarshal(execute("pr", "review", "comments", n, "-R", repo.FullName, "--json"), &comments))
require.Len(t, comments, 1)
require.Equal(t, "change.txt", comments[0].Path)
require.EqualValues(t, 1, comments[0].Line)
commentID := fmt.Sprint(comments[0].ID)
require.Contains(t, string(execute("pr", "review", "reply", n, commentID, "-R", repo.FullName, "--body", "Disposable reply", "--json", "body")), "Disposable reply")
require.NoError(t, json.Unmarshal(execute("pr", "review", "comments", n, "-R", repo.FullName, "--json"), &comments))
require.Len(t, comments, 2)
require.Equal(t, "Disposable reply", comments[1].Body)
out.Reset()
diagnostics.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "review", "resolve", commentID, "-R", repo.FullName}))
require.Empty(t, out.String())
require.Contains(t, diagnostics.String(), "resolved")
require.NoError(t, json.Unmarshal(execute("pr", "review", "comments", n, "-R", repo.FullName, "--review", fmt.Sprint(review.ID), "--json"), &comments))
require.NotEmpty(t, comments[0].Resolver)
out.Reset()
diagnostics.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "review", "unresolve", commentID, "-R", repo.FullName}))
require.Empty(t, out.String())
require.NoError(t, json.Unmarshal(execute("pr", "review", "comments", n, "-R", repo.FullName, "--json"), &comments))
require.Empty(t, comments[0].Resolver)
var pending services.Review
require.NoError(t, json.Unmarshal(execute("pr", "review", n, "-R", repo.FullName, "--event", "PENDING", "--body", "Preserved draft text", "--json"), &pending))
require.Equal(t, "PENDING", pending.State)
var submitted services.Review
require.NoError(t, json.Unmarshal(executeWithEditorIntegration(t, deps, pending.Body, "Edited draft text", "pr", "review", "submit", n, fmt.Sprint(pending.ID), "-R", repo.FullName, "--event", "COMMENT"), &submitted))
require.Equal(t, "COMMENT", submitted.State)
require.Equal(t, "Edited draft text", submitted.Body)
out.Reset()
diagnostics.Reset()
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "review", "delete", n, fmt.Sprint(submitted.ID), "-R", repo.FullName, "--yes"}))
verifyEditorSubmission(t, deps, repo.FullName, n, true)
require.Empty(t, out.String())
require.Contains(t, diagnostics.String(), "Deleted review")
if reviewerTokenFile := os.Getenv("GITEA_TEA_TEST_REVIEWER_TOKEN_FILE"); reviewerTokenFile != "" {
reviewerDeps := deps
reviewerDeps.Getenv = gitea.env(readTokenFile(t, reviewerTokenFile))
out.Reset()
require.NoError(t, Execute(context.Background(), reviewerDeps, []string{"api", "user"}))
var reviewer struct{ Login string }
require.NoError(t, json.Unmarshal(out.Bytes(), &reviewer))
require.NotEmpty(t, reviewer.Login)
deps.Streams.In = strings.NewReader(`{"permission":"write"}`)
execute("api", "repos/"+repo.FullName+"/collaborators/"+reviewer.Login, "--method", "PUT", "--input", "-")
var requested services.Pull
require.NoError(t, json.Unmarshal(execute("pr", "edit", n, "-R", repo.FullName, "--add-reviewers", reviewer.Login, "--json"), &requested))
require.Contains(t, requested.Reviewers, reviewer.Login)
require.Empty(t, requested.ReviewerTeams)
out.Reset()
require.NoError(t, Execute(context.Background(), reviewerDeps, []string{"pr", "status", "-R", repo.FullName, "--json"}))
require.NoError(t, json.Unmarshal(out.Bytes(), &pullStatus))
require.Empty(t, pullStatus.Authored)
require.Len(t, pullStatus.ReviewRequested, 1)
require.Equal(t, pull.Number, pullStatus.ReviewRequested[0].Number)
require.Equal(t, repo.FullName, pullStatus.ReviewRequested[0].Repository)
require.NoError(t, json.Unmarshal(execute("pr", "review", "cancel-request", n, "-R", repo.FullName, "--reviewer", reviewer.Login, "--json"), &requested))
require.NotContains(t, requested.Reviewers, reviewer.Login)
require.NoError(t, json.Unmarshal(execute("pr", "review", "request", n, "-R", repo.FullName, "--reviewer", reviewer.Login, "--json"), &requested))
require.Contains(t, requested.Reviewers, reviewer.Login)
require.NoError(t, json.Unmarshal(execute("pr", "edit", n, "-R", repo.FullName, "--remove-reviewers", reviewer.Login, "--body", "Updated alongside review request", "--json"), &requested))
require.NotContains(t, requested.Reviewers, reviewer.Login)
require.Equal(t, "Updated alongside review request", requested.Body)
execute("pr", "review", "request", n, "-R", repo.FullName, "--reviewer", reviewer.Login)
out.Reset()
require.NoError(t, Execute(context.Background(), reviewerDeps, []string{"pr", "review", n, "-R", repo.FullName, "--event", "APPROVED", "--json"}))
var approval services.Review
require.NoError(t, json.Unmarshal(out.Bytes(), &approval))
require.Equal(t, "APPROVED", approval.State)
out.Reset()
require.NoError(t, Execute(context.Background(), reviewerDeps, []string{"pr", "status", "-R", repo.FullName, "--json"}))
require.NoError(t, json.Unmarshal(out.Bytes(), &pullStatus))
require.Empty(t, pullStatus.ReviewRequested)
require.NoError(t, json.Unmarshal(executeWithEditorIntegration(t, deps, "", "Disposable dismissal reason", "pr", "review", "dismiss", n, fmt.Sprint(approval.ID), "-R", repo.FullName, "--yes"), &approval))
require.True(t, approval.Dismissed)
require.NoError(t, json.Unmarshal(execute("pr", "review", "undismiss", n, fmt.Sprint(approval.ID), "-R", repo.FullName, "--json"), &approval))
require.False(t, approval.Dismissed)
testTeamReviewRequests(t, &deps, execute, reviewer.Login)
}
execute("pr", "close", n, "-R", repo.FullName)
var pulls []services.Pull
require.NoError(t, json.Unmarshal(execute("pr", "list", "--state=closed", "-R", repo.FullName, "--json"), &pulls))
require.Len(t, pulls, 1)
require.Equal(t, "closed", pulls[0].State)
}
func testTeamReviewRequests(t *testing.T, deps *Dependencies, execute func(...string) []byte, reviewer string) {
t.Helper()
orgName := fmt.Sprintf("tea-review-org-%d", time.Now().UnixNano())
execute("org", "create", orgName, "--visibility", "private")
t.Cleanup(func() {
if err := Execute(context.Background(), *deps, []string{"org", "delete", orgName, "--yes"}); err != nil {
t.Errorf("review organization cleanup: %v", err)
}
})
var team services.Team
require.NoError(t, json.Unmarshal(execute("team", "create", orgName, "--name", "Reviewers", "--unit", "repo.code=read,repo.pulls=write", "--json"), &team))
teamID := fmt.Sprint(team.ID)
execute("team", "member", "add", teamID, reviewer)
var repo services.Repository
require.NoError(t, json.Unmarshal(execute("repo", "create", "review-target", "--org", orgName, "--auto-init", "--private", "--json"), &repo))
t.Cleanup(func() {
if err := Execute(context.Background(), *deps, []string{"repo", "delete", "-R", repo.FullName, "--yes"}); err != nil {
t.Errorf("team review repository cleanup: %v", err)
}
})
execute("team", "repo", "add", teamID, "-R", repo.FullName)
execute("branch", "create", "feature", "-R", repo.FullName)
deps.Streams.In = strings.NewReader(`{"branch":"feature","content":"dGVzdAo=","message":"Disposable team review change"}`)
execute("api", "repos/"+repo.FullName+"/contents/change.txt", "--input", "-")
var pull services.Pull
require.NoError(t, json.Unmarshal(execute("pr", "create", "-R", repo.FullName, "--title", "Team review fixture", "--head", "feature", "--base", repo.DefaultBranch, "--json"), &pull))
n := fmt.Sprint(pull.Number)
require.NoError(t, json.Unmarshal(execute("pr", "edit", n, "-R", repo.FullName, "--add-reviewer-teams", team.Name, "--json"), &pull))
require.Equal(t, []string{team.Name}, pull.ReviewerTeams)
require.Empty(t, pull.Reviewers)
require.NoError(t, json.Unmarshal(execute("pr", "review", "cancel-request", n, "-R", repo.FullName, "--team", team.Name, "--json"), &pull))
require.Empty(t, pull.ReviewerTeams)
require.NoError(t, json.Unmarshal(execute("pr", "review", "request", n, "-R", repo.FullName, "--team", team.Name, "--json"), &pull))
require.Equal(t, []string{team.Name}, pull.ReviewerTeams)
require.NoError(t, json.Unmarshal(execute("pr", "edit", n, "-R", repo.FullName, "--remove-reviewer-teams", team.Name, "--json"), &pull))
require.Empty(t, pull.ReviewerTeams)
}
func assertEditedDeadline(t *testing.T, value *string, day string) {
t.Helper()
require.NotNil(t, value)
date, err := time.Parse(time.RFC3339, *value)
require.NoError(t, err)
require.Equal(t, day, date.Format("2006-01-02"))
require.Equal(t, 59, date.Minute())
require.Equal(t, 59, date.Second())
}