// Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT package drive import ( "context" "fmt" "strings" "testing" "time" clie2e "github.com/larksuite/cli/tests/cli_e2e" "github.com/tidwall/gjson" ) const ( driveDeleteVisibilityTimeout = 30 * time.Second driveDeleteVisibilityPoll = 3 * time.Second ) var driveDeleteVisibilityWait = clie2e.WaitOptions{ // This wait only covers the post-delete visibility lag after Drive accepts // deletion. The delete command itself is bounded by clie2e.CleanupContext. Timeout: driveDeleteVisibilityTimeout, Interval: driveDeleteVisibilityPoll, } // CreateDriveFolder creates a Drive folder, optionally under a parent folder, and // deletes it during parent cleanup. func CreateDriveFolder(t *testing.T, parentT *testing.T, ctx context.Context, name string, defaultAs string, parentFolderToken string) string { t.Helper() if defaultAs == "" { defaultAs = "bot" } args := []string{"drive", "+create-folder", "--name", name} if parentFolderToken != "" { args = append(args, "--folder-token", parentFolderToken) } result, err := clie2e.RunCmd(ctx, clie2e.Request{ Args: args, DefaultAs: defaultAs, }) if err != nil { t.Fatalf("create drive folder %q: %v", name, err) } result.AssertExitCode(t, 0) result.AssertStdoutStatus(t, true) folderToken := gjson.Get(result.Stdout, "data.folder_token").String() if folderToken == "" { t.Fatalf("drive folder token should not be empty, stdout:\n%s", result.Stdout) } parentT.Cleanup(func() { cleanupCtx, cancel := clie2e.CleanupContext() defer cancel() deleteResult, deleteErr := DeleteDriveResourceAndVerify(cleanupCtx, folderToken, "folder", defaultAs) clie2e.ReportCleanupFailure(parentT, "delete drive folder "+folderToken, deleteResult, deleteErr) }) return folderToken } // DeleteDriveResourceAndVerify deletes a Drive-backed resource, then polls // drive meta until the token is either gone or no longer has an accessible URL. // This prevents cleanup from looking successful when the delete command // returned a suppressed not_found or partial API error but the resource still // exists. func DeleteDriveResourceAndVerify(ctx context.Context, token, docType, defaultAs string) (*clie2e.Result, error) { return deleteDriveResourceAndVerify(ctx, token, docType, defaultAs, driveDeleteVisibilityWait) } func deleteDriveResourceAndVerify(ctx context.Context, token, docType, defaultAs string, visibilityWait clie2e.WaitOptions) (*clie2e.Result, error) { if defaultAs == "" { defaultAs = "bot" } deleteResult, deleteErr := clie2e.RunCmd(ctx, clie2e.Request{ Args: []string{"drive", "+delete", "--file-token", token, "--type", docType, "--yes"}, DefaultAs: defaultAs, }) if deleteErr != nil || deleteResult == nil { return deleteResult, deleteErr } if deleteResult.ExitCode != 0 { deleted, verifyErr := IsDriveResourceDeleted(ctx, token, docType, defaultAs) if verifyErr != nil { return deleteResult, verifyErr } if deleted { deleteResult.ExitCode = 0 return deleteResult, nil } return deleteResult, fmt.Errorf("drive resource %s/%s still exists after delete failed: exit=%d stdout=%s stderr=%s", docType, token, deleteResult.ExitCode, deleteResult.Stdout, deleteResult.Stderr) } if err := waitDriveResourceDeleted(ctx, token, docType, defaultAs, visibilityWait); err != nil { return deleteResult, clie2e.CleanupWarning( fmt.Errorf("drive resource %s/%s still visible after accepted delete: %w", docType, token, err), ) } return deleteResult, nil } func waitDriveResourceDeleted(ctx context.Context, token, docType, defaultAs string, opts clie2e.WaitOptions) error { opts.TimeoutError = func() error { return fmt.Errorf("drive resource %s/%s still exists %s after delete", docType, token, opts.Timeout) } return clie2e.WaitForCondition(ctx, opts, func() (bool, error) { return IsDriveResourceDeleted(ctx, token, docType, defaultAs) }) } func IsDriveResourceDeleted(ctx context.Context, token, docType, defaultAs string) (bool, error) { if defaultAs == "" { defaultAs = "bot" } result, err := clie2e.RunCmd(ctx, clie2e.Request{ Args: []string{"api", "post", "/open-apis/drive/v1/metas/batch_query"}, DefaultAs: defaultAs, Data: map[string]any{ "request_docs": []map[string]any{{ "doc_token": token, "doc_type": docType, }}, }, }) if err != nil { return false, err } if result.ExitCode != 0 { combined := strings.ToLower(result.Stdout + "\n" + result.Stderr) if strings.Contains(combined, "not found") || strings.Contains(combined, "404") { return true, nil } return false, fmt.Errorf("verify drive resource %s/%s after delete: exit=%d stdout=%s stderr=%s", docType, token, result.ExitCode, result.Stdout, result.Stderr) } if !isDriveMetaQuerySuccessful(result.Stdout) { return false, fmt.Errorf("verify drive resource %s/%s after delete returned unsuccessful envelope: stdout=%s stderr=%s", docType, token, result.Stdout, result.Stderr) } metas := gjson.Get(result.Stdout, "data.metas").Array() if len(metas) == 0 { return true, nil } if docType != "folder" { allURLsCleared := true for _, meta := range metas { if meta.Get("url").String() != "" { allURLsCleared = false break } } if allURLsCleared { return true, nil } } return false, nil } func isDriveMetaQuerySuccessful(stdout string) bool { if ok := gjson.Get(stdout, "ok"); ok.Exists() { return ok.Bool() } if code := gjson.Get(stdout, "code"); code.Exists() { return code.Int() == 0 } return false }