From 8196f3259e562d6cafdbc23273c5342852337631 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Mon, 16 Feb 2026 06:50:17 -0500 Subject: [PATCH 01/14] refactor: replace testify with stdlib test helpers Replace all testify/assert and testify/require usage across 69 test files with shared/testutil assertion helpers. This aligns with STANDARDS.md Section 14: no testify, no gomega, no ginkgo. - Add shared/testutil/assert.go with Equal, NoError, Contains, True, False, Nil, NotNil, Len, Empty, and Require* variants - Convert all test files in shared/, tools/cfl/, and tools/jtk/ - Remove testify dependency from all three go.mod files - Add STANDARDS.md to repo root - Update docs to reference shared/testutil Refs #159 --- STANDARDS.md | 1529 +++++++++++++++++ shared/adf/convert_test.go | 328 ++-- shared/go.mod | 4 - shared/go.sum | 10 - shared/prompt/confirm_test.go | 15 +- shared/testutil/assert.go | 229 +++ tools/cfl/CLAUDE.md | 2 +- tools/cfl/api/attachments_test.go | 55 +- tools/cfl/api/client_test.go | 33 +- tools/cfl/api/pages_test.go | 233 ++- tools/cfl/api/search_test.go | 91 +- tools/cfl/api/space_management_test.go | 109 +- tools/cfl/api/spaces_test.go | 95 +- tools/cfl/go.mod | 2 - tools/cfl/go.sum | 4 - .../internal/cmd/attachment/delete_test.go | 37 +- .../internal/cmd/attachment/download_test.go | 49 +- .../cfl/internal/cmd/attachment/list_test.go | 37 +- .../internal/cmd/attachment/upload_test.go | 33 +- .../cmd/completion/completion_test.go | 49 +- .../cfl/internal/cmd/configcmd/clear_test.go | 29 +- tools/cfl/internal/cmd/configcmd/show_test.go | 10 +- tools/cfl/internal/cmd/configcmd/test_test.go | 13 +- tools/cfl/internal/cmd/init/init_test.go | 75 +- tools/cfl/internal/cmd/page/copy_test.go | 39 +- tools/cfl/internal/cmd/page/create_test.go | 131 +- tools/cfl/internal/cmd/page/delete_test.go | 29 +- tools/cfl/internal/cmd/page/edit_test.go | 181 +- tools/cfl/internal/cmd/page/fetch_test.go | 41 +- tools/cfl/internal/cmd/page/list_test.go | 47 +- tools/cfl/internal/cmd/page/view_test.go | 99 +- tools/cfl/internal/cmd/search/search_test.go | 81 +- tools/cfl/internal/cmd/space/list_test.go | 53 +- tools/cfl/internal/cmd/space/space_test.go | 131 +- tools/cfl/internal/config/config_test.go | 71 +- tools/cfl/pkg/md/codeprotect_test.go | 78 +- tools/cfl/pkg/md/converter_test.go | 159 +- tools/cfl/pkg/md/from_adf_test.go | 171 +- tools/cfl/pkg/md/from_html_test.go | 119 +- tools/cfl/pkg/md/macro_test.go | 33 +- tools/cfl/pkg/md/parser_test.go | 157 +- tools/cfl/pkg/md/render_test.go | 60 +- tools/cfl/pkg/md/roundtrip_test.go | 131 +- tools/cfl/pkg/md/to_adf_test.go | 434 ++--- tools/cfl/pkg/md/tokenizer_bracket_test.go | 193 ++- tools/cfl/pkg/md/tokenizer_xml_test.go | 249 ++- tools/cfl/pkg/md/wikilink_test.go | 195 ++- tools/jtk/CLAUDE.md | 4 +- tools/jtk/api/attachments_test.go | 88 +- tools/jtk/api/automation_test.go | 157 +- tools/jtk/api/client_test.go | 52 +- tools/jtk/api/errors_test.go | 34 +- tools/jtk/api/field_management_test.go | 148 +- tools/jtk/api/fields_test.go | 68 +- tools/jtk/api/markdown_test.go | 288 ++-- tools/jtk/api/move_test.go | 107 +- tools/jtk/api/projects_test.go | 116 +- tools/jtk/api/transitions_test.go | 68 +- tools/jtk/api/types_test.go | 183 +- tools/jtk/api/users_test.go | 40 +- tools/jtk/api/wiki_test.go | 19 +- tools/jtk/go.mod | 4 - tools/jtk/go.sum | 9 - .../internal/cmd/automation/create_test.go | 57 +- .../internal/cmd/automation/enable_test.go | 27 +- tools/jtk/internal/cmd/automation/get_test.go | 6 +- .../internal/cmd/automation/update_test.go | 15 +- .../internal/cmd/comments/comments_test.go | 63 +- .../internal/cmd/configcmd/configcmd_test.go | 75 +- tools/jtk/internal/cmd/fields/fields_test.go | 213 ++- .../jtk/internal/cmd/initcmd/initcmd_test.go | 10 +- .../jtk/internal/cmd/issues/assignee_test.go | 35 +- tools/jtk/internal/cmd/issues/create_test.go | 141 +- tools/jtk/internal/cmd/issues/get_test.go | 47 +- tools/jtk/internal/cmd/issues/types_test.go | 61 +- tools/jtk/internal/cmd/issues/update_test.go | 147 +- .../internal/cmd/projects/projects_test.go | 119 +- .../cmd/transitions/transitions_test.go | 14 +- tools/jtk/internal/config/config_test.go | 117 +- 79 files changed, 5069 insertions(+), 3416 deletions(-) create mode 100644 STANDARDS.md create mode 100644 shared/testutil/assert.go diff --git a/STANDARDS.md b/STANDARDS.md new file mode 100644 index 0000000..7bedb89 --- /dev/null +++ b/STANDARDS.md @@ -0,0 +1,1529 @@ +# Go CLI Style Guide + +This document catalogs coding conventions for Go CLI tools. It is intended for use as an operationalized code review prompt for AI-assisted review, but is also useful as a human reference. + +When reviewing code, flag deviations from these patterns. Be pragmatic: the goal is consistency within a codebase, not pedantic enforcement. If a deviation improves readability or correctness, note it as an intentional departure rather than a defect. + +### Guiding Philosophy + +Prefer clarity, composability, and maintainability over cleverness. Go's strength is boringly readable code — lean into that. Use the standard library aggressively. Resist the urge to abstract prematurely or import a dependency for something you can write in 20 lines. Use judgement, not dogma. + +--- + +## 1. Project Configuration + +### Module Layout + +Every tool gets its own `go.mod`. For a monorepo with shared libraries, use a top-level module with internal packages: + +``` +tools/ +├── go.mod +├── go.sum +├── cmd/ +│ ├── ingest/ +│ │ └── main.go +│ ├── reconcile/ +│ │ └── main.go +│ └── sync/ +│ └── main.go +├── internal/ +│ ├── config/ +│ ├── logging/ +│ └── aws/ +└── pkg/ # only if genuinely intended for external consumption +``` + +`internal/` is the default for shared code. `pkg/` is only for packages explicitly designed as public API for other modules. When in doubt, use `internal/`. + +### Build Configuration + +Pin the Go version in `go.mod` and use a `.tool-versions` or `go.env` for the team: + +``` +go 1.24 +``` + +Use `go.sum` for reproducible builds. Run `go mod tidy` before every commit — CI should fail if `go.mod` and `go.sum` are dirty. + +### Linting + +All projects use `golangci-lint` with a shared `.golangci.yml`. At minimum, enable: + +```yaml +linters: + enable: + - errcheck + - govet + - staticcheck + - unused + - ineffassign + - misspell + - revive + - gosec + - errorlint # enforce error wrapping best practices + - exhaustive # enforce exhaustive switch/select on enums +``` + +`go vet` and `staticcheck` findings are non-negotiable. Treat them as errors in CI. + +### Dependency Hygiene + +Order imports in three groups separated by blank lines: standard library, external dependencies, internal packages: + +```go +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + "go.uber.org/zap" + + "github.com/yourorg/tools/internal/config" +) +``` + +`goimports` enforces this automatically. Run it on save. + +### Makefile + +Every repo has a `Makefile` at the root. This is the answer to "I just cloned this repo, now what." CI runs the same targets developers run locally. + +```makefile +.PHONY: build lint test tidy check + +# Build all binaries into bin/ +build: + go build -o bin/ ./cmd/... + +# Lint with golangci-lint (config in .golangci.yml) +lint: + golangci-lint run ./... + +# Run tests with race detector +test: + go test -race ./... + +# Tidy and verify modules are clean +tidy: + go mod tidy + git diff --exit-code go.mod go.sum + +# CI gate: everything that must pass before merge +check: tidy lint test build +``` + +**Rules:** + +- `make check` is the CI gate. It must pass before merge. Run it locally before pushing. +- `make build` outputs all binaries to `bin/`. Add `bin/` to `.gitignore`. +- `make tidy` fails if `go.mod` or `go.sum` are dirty — this catches forgotten `go mod tidy` runs. +- Add tool-specific targets as needed (`make migrate`, `make generate`, `make integration-test`), but the four core targets (`build`, `lint`, `test`, `tidy`) are non-negotiable. +- Keep targets simple. If a target exceeds ~5 lines of shell, it belongs in a script in `scripts/` that the Makefile calls. + +--- + +## 2. Type Design + +### Structs for Data, Methods for Behavior + +Go doesn't have records, but the same instinct applies: separate data-carrying types from service types. Data structs should be plain, exported fields. Service types hold dependencies and attach methods: + +```go +// Data: plain struct, no methods beyond serialization +type SyncResult struct { + CompanyID string + Success bool + FailedKeys []string + Duration time.Duration +} + +// Service: holds dependencies, has methods +type Reconciler struct { + db *sql.DB + logger *slog.Logger + clock func() time.Time // injectable for testing +} +``` + +### Prefer Value Semantics for Small Types + +Small data types (< ~128 bytes, no mutability needs) should be passed and returned by value, not pointer. This is Go's equivalent of preferring value types: + +```go +// Good: small, immutable-ish, pass by value +type Tenant struct { + ID string + Name string +} + +type DateRange struct { + Start time.Time + End time.Time +} + +// Pointer receiver appropriate: large struct or needs mutation +type IngestionState struct { + // ... many fields, mutated over lifetime +} + +func (s *IngestionState) MarkComplete(key string) { ... } +``` + +### Strongly-Typed Identifiers + +Wrap primitive identifiers in named types to prevent parameter confusion: + +```go +type TenantID string +type CompanyID string +type BusinessID string + +func GetConnection(tenant TenantID, company CompanyID) (*Connection, error) { ... } +``` + +This makes `GetConnection(companyID, tenantID)` a compile error instead of a subtle bug. Use sparingly — only where misorderings are a real risk (multiple string parameters of the same shape). + +### Constructor Functions + +Use `NewX` functions when a type requires initialization, validation, or has unexported fields. Return the concrete type, not an interface: + +```go +func NewReconciler(db *sql.DB, logger *slog.Logger, opts ...Option) *Reconciler { + r := &Reconciler{ + db: db, + logger: logger, + clock: time.Now, + } + for _, opt := range opts { + opt(r) + } + return r +} +``` + +For simple structs with all-exported fields, struct literals are fine — no constructor needed. + +### Enums via Constants + +Go lacks sum types. Use typed constants with `iota`, and always handle the zero value explicitly: + +```go +type PlatformType int + +const ( + PlatformUnknown PlatformType = iota // zero value = unknown + PlatformAccounting + PlatformBanking + PlatformCommerce +) + +func (p PlatformType) String() string { + switch p { + case PlatformAccounting: + return "Accounting" + case PlatformBanking: + return "Banking" + case PlatformCommerce: + return "Commerce" + default: + return fmt.Sprintf("PlatformType(%d)", p) + } +} +``` + +For cases where you need exhaustiveness checking, `exhaustive` lint catches missing switch arms. + +--- + +## 3. Interface Design + +### Accept Interfaces, Return Structs + +This is the single most important Go design principle. Define interfaces at the call site (consumer), not at the implementation: + +```go +// Good: interface defined where it's consumed, not where it's implemented +// In reconciler.go: +type AccountFetcher interface { + GetAccounts(ctx context.Context, tenant TenantID) ([]Account, error) +} + +type Reconciler struct { + accounts AccountFetcher + // ... +} + +// In accounts.go — no interface declared here, just a concrete type +type AccountStore struct { + db *sql.DB +} + +func (s *AccountStore) GetAccounts(ctx context.Context, tenant TenantID) ([]Account, error) { ... } +``` + +### Keep Interfaces Small + +One to three methods is ideal. If an interface has more than five methods, it's probably doing too much. The standard library's `io.Reader` (one method) is the gold standard. + +```go +// Good: focused interface +type TokenRefresher interface { + RefreshToken(ctx context.Context, tenant TenantID) (Token, error) +} + +// Suspicious: interface is a service dump +type BusinessManager interface { + GetBusiness(ctx context.Context, id string) (*Business, error) + CreateBusiness(ctx context.Context, b Business) error + UpdateBusiness(ctx context.Context, b Business) error + DeleteBusiness(ctx context.Context, id string) error + ListBusinesses(ctx context.Context, tenant TenantID) ([]Business, error) + GetBusinessConnections(ctx context.Context, id string) ([]Connection, error) + // ... this is just a struct with extra steps +} +``` + +### The Empty Interface Smell + +`any` (`interface{}`) in function signatures is almost always a design smell. It means "I gave up on types." Acceptable uses: logging arguments, JSON marshaling boundaries, generic containers. Unacceptable: core business logic parameters. + +--- + +## 4. Error Handling + +### Errors Are Values, Not Exceptions + +Every function that can fail returns an `error`. Check it immediately. Never discard errors silently: + +```go +// Good: check immediately, handle or propagate +result, err := store.GetAccounts(ctx, tenant) +if err != nil { + return nil, fmt.Errorf("fetching accounts for %s: %w", tenant, err) +} +``` + +### Wrapping With Context + +Always wrap errors with `fmt.Errorf("context: %w", err)` to build a trace. The message should describe what *this* function was trying to do, not repeat what the callee said: + +```go +// Good: each layer adds its own context +func (r *Reconciler) Run(ctx context.Context, tenant TenantID) error { + accounts, err := r.accounts.GetAccounts(ctx, tenant) + if err != nil { + return fmt.Errorf("reconciling tenant %s: %w", tenant, err) + } + // ... +} + +// Bad: restating the callee's error +if err != nil { + return fmt.Errorf("failed to get accounts: %w", err) // "failed to" is noise +} +``` + +### Sentinel Errors and Error Types + +Define sentinel errors for conditions callers need to match on. Use custom error types when callers need structured data: + +```go +var ( + ErrNotFound = errors.New("not found") + ErrAlreadyExists = errors.New("already exists") + ErrRateLimited = errors.New("rate limited") +) + +// Callers check with errors.Is: +if errors.Is(err, ErrNotFound) { + // handle missing resource +} + +// Custom error type when callers need details: +type ValidationError struct { + Field string + Message string +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("validation: %s: %s", e.Field, e.Message) +} + +// Callers check with errors.As: +var ve *ValidationError +if errors.As(err, &ve) { + fmt.Printf("bad field: %s\n", ve.Field) +} +``` + +### Don't Panic + +`panic` is for programmer bugs (impossible states, violated invariants in init), never for runtime errors. A CLI tool that panics on bad input is broken. Recover from panics only at the outermost boundary (e.g., a top-level middleware in a server, or the root command's `RunE`). + +### Eliminate `else` After Error Returns + +Go's error handling naturally produces guard clauses. Embrace them — never nest the happy path inside `else`: + +```go +// Good: guard clause, happy path is un-indented +token, err := auth.GetToken(ctx, tenant) +if err != nil { + return fmt.Errorf("getting token: %w", err) +} +// continue with token... + +// Bad: unnecessary nesting +token, err := auth.GetToken(ctx, tenant) +if err == nil { + // happy path buried in a branch +} else { + return err +} +``` + +--- + +## 5. Context Propagation + +### Context Is Always the First Parameter + +Every function that does I/O, calls other services, or could be cancelled takes `context.Context` as its first parameter. Named `ctx`: + +```go +func (s *SyncService) Sync(ctx context.Context, tenant TenantID, companyID CompanyID) error +``` + +### Never Store Context in Structs + +Context is request-scoped. Storing it in a struct means you're holding onto a cancelled context or sharing one across requests: + +```go +// Bad: context outlives the request +type Worker struct { + ctx context.Context // don't do this +} + +// Good: pass per-call +func (w *Worker) Process(ctx context.Context, job Job) error { ... } +``` + +### Respect Cancellation + +Check `ctx.Err()` or use `select` on `ctx.Done()` in loops and before expensive operations: + +```go +for _, batch := range batches { + if err := ctx.Err(); err != nil { + return fmt.Errorf("cancelled during batch processing: %w", err) + } + if err := processBatch(ctx, batch); err != nil { + return err + } +} +``` + +--- + +## 6. CLI Patterns + +### Cobra for All Tools + +Use Cobra for every CLI tool, even single-command ones. The cognitive cost of "which framework did this tool use" is worse than the tiny overhead of Cobra on a simple tool. Cobra gives you consistent `--help`, flag parsing, subcommand structure, and shell completion for free. Standardize on it and stop thinking about it. + +```go +func main() { + root := &cobra.Command{ + Use: "mytool", + Short: "Does the thing", + // No Run on root — forces subcommand usage + } + + root.AddCommand( + newSyncCmd(), + newReconcileCmd(), + newReportCmd(), + ) + + if err := root.Execute(); err != nil { + os.Exit(1) + } +} +``` + +### Command Factory Functions + +Each subcommand lives in its own file and returns a `*cobra.Command`. Wire dependencies in the `RunE` closure: + +```go +func newSyncCmd() *cobra.Command { + var ( + tenant string + dryRun bool + workers int + ) + + cmd := &cobra.Command{ + Use: "sync [company-id]", + Short: "Sync data for a company", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + companyID := args[0] + + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + + logger := logging.New(cfg.LogLevel) + db, err := openDB(ctx, cfg.DatabaseURL) + if err != nil { + return fmt.Errorf("connecting to database: %w", err) + } + defer db.Close() + + svc := NewSyncService(db, logger) + return svc.Run(ctx, TenantID(tenant), CompanyID(companyID), dryRun) + }, + } + + cmd.Flags().StringVar(&tenant, "tenant", "", "tenant identifier (required)") + cmd.Flags().BoolVar(&dryRun, "dry-run", false, "preview changes without writing") + cmd.Flags().IntVar(&workers, "workers", 4, "number of parallel workers") + _ = cmd.MarkFlagRequired("tenant") + + return cmd +} +``` + +### Exit Codes + +Use distinct exit codes for different failure modes. Define them as constants: + +```go +const ( + ExitOK = 0 + ExitUsageError = 1 + ExitRuntimeError = 2 + ExitConfigError = 3 + ExitPartialFailure = 4 +) +``` + +Cobra handles exit code 1 for usage errors by default. For other cases, handle in `main`: + +```go +func main() { + if err := root.Execute(); err != nil { + var cfgErr *config.Error + if errors.As(err, &cfgErr) { + os.Exit(ExitConfigError) + } + os.Exit(ExitRuntimeError) + } +} +``` + +### Stdin/Stdout/Stderr Discipline + +Standard output is for *data* (pipeable results). Standard error is for *diagnostics* (logs, progress, errors). Never mix them: + +```go +// Data goes to stdout — can be piped to jq, another tool, etc. +enc := json.NewEncoder(os.Stdout) +enc.Encode(result) + +// Diagnostics go to stderr +logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) +``` + +If the tool's primary output is human-readable (not piped), stdout is fine for both, but design for the pipeable case first. + +### Signal Handling + +CLI tools should handle SIGINT/SIGTERM gracefully. Use `signal.NotifyContext` for cancellation: + +```go +func main() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + if err := run(ctx); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} +``` + +--- + +## 7. Configuration + +### Layered Config: Env > Flags > File > Defaults + +Configuration sources, in precedence order: environment variables override flags, flags override file values, file values override defaults. Use a single config struct: + +```go +type Config struct { + DatabaseURL string `env:"DATABASE_URL" json:"database_url"` + LogLevel string `env:"LOG_LEVEL" json:"log_level"` + Workers int `env:"WORKERS" json:"workers"` + Timeout time.Duration `env:"TIMEOUT" json:"timeout"` + DryRun bool // flag-only, no file/env +} + +func Load() (*Config, error) { + cfg := Config{ + LogLevel: "info", + Workers: 4, + Timeout: 30 * time.Second, + } + + // Load from file if present, then overlay env vars + // ... + + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + return &cfg, nil +} +``` + +### Validate Early, Fail Fast + +Validate all configuration at startup before doing any work. A config error 30 minutes into a batch job is a waste: + +```go +func (c *Config) Validate() error { + if c.DatabaseURL == "" { + return errors.New("DATABASE_URL is required") + } + if c.Workers < 1 || c.Workers > 64 { + return fmt.Errorf("workers must be 1-64, got %d", c.Workers) + } + return nil +} +``` + +### Testable Time + +Inject a clock function instead of calling `time.Now()` directly. This is the same principle as C#'s `TimeProvider`: + +```go +// In production +svc := &Service{clock: time.Now} + +// In tests +svc := &Service{clock: func() time.Time { return fixedTime }} +``` + +For more complex time needs, define a small interface: + +```go +type Clock interface { + Now() time.Time +} +``` + +--- + +## 8. Concurrency + +### Start Goroutines, Manage Lifetimes + +Every goroutine must have a clear shutdown path. Use `context.Context` for cancellation and `sync.WaitGroup` or `errgroup.Group` for completion: + +```go +g, ctx := errgroup.WithContext(ctx) + +for _, job := range jobs { + g.Go(func() error { + return processJob(ctx, job) + }) +} + +if err := g.Wait(); err != nil { + return fmt.Errorf("processing jobs: %w", err) +} +``` + +### errgroup for Parallel Tasks + +`errgroup` is the default for parallel work in CLI tools. It handles cancellation on first error and waitgroup semantics in one package: + +```go +g, ctx := errgroup.WithContext(ctx) +g.SetLimit(workers) // bounded parallelism + +for _, item := range items { + g.Go(func() error { + return process(ctx, item) + }) +} +return g.Wait() +``` + +### Channels for Pipelines, Not for Synchronization + +Use channels when data flows between stages. Use `sync.WaitGroup`, `errgroup`, or `sync.Mutex` for synchronization. Don't use a `chan struct{}` when a `WaitGroup` is clearer: + +```go +// Good: channel as a pipeline stage +func produce(ctx context.Context, items []Item) <-chan Item { + ch := make(chan Item) + go func() { + defer close(ch) + for _, item := range items { + select { + case ch <- item: + case <-ctx.Done(): + return + } + } + }() + return ch +} +``` + +### Never Launch Unbounded Goroutines + +Always limit concurrency for I/O-bound work. A CLI tool that launches 10,000 goroutines to hit an API will get rate-limited or OOM. Use `errgroup.SetLimit`, a semaphore channel, or a worker pool: + +```go +// Semaphore pattern +sem := make(chan struct{}, maxConcurrency) +for _, item := range items { + sem <- struct{}{} + go func() { + defer func() { <-sem }() + process(ctx, item) + }() +} +``` + +--- + +## 9. Data Access + +### database/sql with pgx or lib/pq + +Use `database/sql` as the interface layer. `pgx` is preferred as the driver for PostgreSQL (better performance, native types). Dapper-style explicit SQL applies equally here — write your own queries, don't hide behind an ORM: + +```go +const accountsQuery = ` + WITH target AS ( + SELECT b.id AS business_id + FROM business b + JOIN financial_institution fi ON b.tenant_id = fi.tenant_id + WHERE fi.fi_identifier = $1 AND b.company_id = $2 + ) + SELECT a.id, a.name, a.type + FROM account a + JOIN target t ON a.business_id = t.business_id + ORDER BY a.name +` + +func (s *AccountStore) GetAccounts(ctx context.Context, tenant TenantID, company CompanyID) ([]Account, error) { + rows, err := s.db.QueryContext(ctx, accountsQuery, string(tenant), string(company)) + if err != nil { + return nil, fmt.Errorf("querying accounts: %w", err) + } + defer rows.Close() + + var accounts []Account + for rows.Next() { + var a Account + if err := rows.Scan(&a.ID, &a.Name, &a.Type); err != nil { + return nil, fmt.Errorf("scanning account row: %w", err) + } + accounts = append(accounts, a) + } + return accounts, rows.Err() +} +``` + +### SQL Best Practices + +These carry over directly from the C# guide: + +- Always specify columns — avoid `SELECT *` +- Always use parameterized queries (`$1`, `$2`), never `fmt.Sprintf` into SQL +- Use CTEs over subqueries for readability +- Paginate large result sets; prefer cursor-based pagination over `OFFSET`/`LIMIT` +- Batch large `IN` clauses (100+ items) with `ANY($1::text[])` or temp tables + +### Transaction Management + +```go +tx, err := db.BeginTx(ctx, nil) +if err != nil { + return fmt.Errorf("beginning transaction: %w", err) +} +defer tx.Rollback() // no-op if committed + +// ... operations using tx ... + +if err := tx.Commit(); err != nil { + return fmt.Errorf("committing transaction: %w", err) +} +``` + +The `defer tx.Rollback()` pattern is idiomatic — it's a no-op after a successful commit and ensures cleanup on any error path. + +--- + +## 10. Serialization + +### encoding/json from the Standard Library + +Use `encoding/json` by default. For performance-sensitive paths, `json/v2` (when stable) or `github.com/goccy/go-json` are acceptable drop-in replacements. + +### Struct Tags Are the Schema + +```go +type SyncRequest struct { + TenantID string `json:"tenant_id"` + CompanyID string `json:"company_id"` + Platform PlatformType `json:"platform"` + Priority int `json:"priority,omitempty"` +} +``` + +Use `omitempty` deliberately — it means "omit when zero value," which may or may not be what you want. An `int` field with `omitempty` drops `0`, which may be meaningful. + +### Custom Marshaling for Enums + +```go +func (p PlatformType) MarshalJSON() ([]byte, error) { + return json.Marshal(p.String()) +} + +func (p *PlatformType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + switch s { + case "Accounting": + *p = PlatformAccounting + case "Banking": + *p = PlatformBanking + default: + return fmt.Errorf("unknown platform type: %q", s) + } + return nil +} +``` + +--- + +## 11. Logging + +### slog for CLIs, zap for Services + +Use `log/slog` from the standard library for CLI tools. It's zero-dependency, has the right level of abstraction for console programs, and writes to stderr by default (which is what you want for CLIs — see Section 6 on stdout/stderr discipline). + +For long-running web services where logging is on the hot path, `go.uber.org/zap` is acceptable — it's measurably faster due to pre-allocation and zero-reflection design. But for CLI tools, logging throughput is never the bottleneck, and slog's simplicity wins. + +```go +// CLI: slog with text handler for human-readable output +handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelInfo, +}) +logger := slog.New(handler) + +// CLI: JSON handler when output will be ingested by log aggregation +handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelInfo, +}) +logger := slog.New(handler) + +// Good: structured key-value pairs +logger.Info("calculating insights", + "tenant", tenant, + "company_id", companyID, + "platform", platform, +) + +logger.Error("sync failed", + "tenant", tenant, + "company_id", companyID, + "error", err, + "elapsed_ms", elapsed.Milliseconds(), +) +``` + +### Named Fields, Not Interpolation + +Same principle as the C# guide — each value must be a discrete, queryable field: + +```go +// Good: each field independently queryable +logger.Info("processing transaction", + "tenant", tenant, + "company_id", companyID, + "txn_id", txnID, +) + +// Bad: opaque string defeats structured logging +logger.Info(fmt.Sprintf("%s::%s - processing transaction %s", tenant, companyID, txnID)) +``` + +### Logger Propagation + +Pass loggers as dependencies, not globals. Use `slog.With` to add context that applies to all messages in a scope: + +```go +func (s *SyncService) Run(ctx context.Context, tenant TenantID, company CompanyID) error { + log := s.logger.With("tenant", tenant, "company_id", company) + log.Info("starting sync") + // all subsequent log calls in this scope include tenant and company_id +} +``` + +### Log Levels + +| Level | Usage | +|-------|-------| +| Info | Start/completion of major operations, business events | +| Warn | Retry attempts, degraded scenarios, non-critical issues | +| Error | Failures (always include the error value) | +| Debug | Detailed operational info, only enabled in dev/troubleshooting | + +### Performance Timing + +```go +start := time.Now() +// ... work +logger.Info("operation complete", + "tenant", tenant, + "elapsed_ms", time.Since(start).Milliseconds(), +) +``` + +### Log Security + +Never log sensitive information: passwords, tokens, PII, full credit card numbers, SSNs. Be cautious with user attributes — only log what's necessary for debugging. + +--- + +## 12. Error Handling & Result Patterns + +### Guard Clauses and Early Returns + +Same as C#: reject invalid states early, keep the happy path at the lowest indentation level: + +```go +func (s *Service) Process(ctx context.Context, req Request) (*Result, error) { + if req.TenantID == "" { + return nil, &ValidationError{Field: "tenant_id", Message: "required"} + } + + conn, err := s.getConnection(ctx, req.TenantID) + if err != nil { + return nil, fmt.Errorf("getting connection: %w", err) + } + + // happy path continues un-indented... +} +``` + +### Multi-Value Returns for Outcome Disambiguation + +Go's multiple return values serve the same role as C#'s tuple returns: + +```go +// Found vs not-found vs error are three different outcomes +func (s *Store) GetAccount(ctx context.Context, id string) (account Account, found bool, err error) { + row := s.db.QueryRowContext(ctx, query, id) + if err := row.Scan(&account.ID, &account.Name); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return Account{}, false, nil + } + return Account{}, false, fmt.Errorf("scanning account: %w", err) + } + return account, true, nil +} +``` + +### ok-Pattern for Optional Results + +For lookups that may miss, use the comma-ok pattern familiar from map access: + +```go +val, ok := cache[key] +if !ok { + // handle miss +} +``` + +### Collecting Errors in Batch Operations + +For operations that process multiple items where you want partial results, collect errors rather than failing on the first one: + +```go +var errs []error +for _, item := range items { + if err := process(ctx, item); err != nil { + errs = append(errs, fmt.Errorf("item %s: %w", item.ID, err)) + continue + } +} +if len(errs) > 0 { + return fmt.Errorf("partial failure (%d/%d): %w", len(errs), len(items), errors.Join(errs...)) +} +``` + +--- + +## 13. Collection Patterns + +### Nil Slices Over Empty Slices + +In Go, a nil slice and an empty slice behave identically for `len`, `cap`, `range`, and `append`. Prefer nil (the zero value) — don't allocate when there's nothing to hold: + +```go +// Good: zero value is fine +var accounts []Account +// len(accounts) == 0, range works, append works + +// Unnecessary: allocating for no reason +accounts := make([]Account, 0) +accounts := []Account{} +``` + +Exception: JSON serialization. `json.Marshal(nil)` produces `null`, while `json.Marshal([]Account{})` produces `[]`. If the distinction matters to consumers, initialize explicitly. + +### Pre-Allocate When Size Is Known + +```go +results := make([]Result, 0, len(items)) +for _, item := range items { + results = append(results, transform(item)) +} +``` + +### maps Package for Common Operations + +Use `maps.Keys`, `maps.Values`, `maps.Clone` from the standard library instead of hand-rolling: + +```go +import "maps" + +keys := slices.Sorted(maps.Keys(accountsByID)) +``` + +### slices Package for Transformations + +Use `slices.SortFunc`, `slices.Contains`, `slices.Compact`, etc.: + +```go +import "slices" + +slices.SortFunc(accounts, func(a, b Account) int { + return strings.Compare(a.Name, b.Name) +}) + +hasAdmin := slices.ContainsFunc(roles, func(r Role) bool { + return r.Name == "admin" +}) +``` + +### Chunking for Batch Operations + +Same concept as C#'s `.Chunk()` — batch items for APIs with size limits: + +```go +func Chunk[T any](items []T, size int) [][]T { + var chunks [][]T + for size < len(items) { + items, chunks = items[size:], append(chunks, items[:size]) + } + return append(chunks, items) +} + +// Usage: DynamoDB BatchWriteItem supports max 25 items +for _, batch := range Chunk(writeRequests, 25) { + if err := writeBatch(ctx, batch); err != nil { + return err + } +} +``` + +Or use `slices.Chunk` if available on your Go version. + +--- + +## 14. Testing + +### Framework: Standard Library Only + +Use `testing` from the standard library. No testify, no gomega, no ginkgo. Table-driven tests and `t.Helper()` cover nearly everything. If you need mocks, write them by hand or use a small code generator — a mock framework dependency is almost never worth it. + +### Table-Driven Tests + +The default test structure. Each case is a named struct in a slice: + +```go +func TestGetPrimaryKeyName(t *testing.T) { + tests := []struct { + name string + platform PlatformType + want string + wantErr bool + }{ + {name: "accounting", platform: PlatformAccounting, want: "companyAndInsight"}, + {name: "banking", platform: PlatformBanking, want: "extCompanyId"}, + {name: "unknown panics", platform: PlatformUnknown, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetPrimaryKeyName(tt.platform) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("GetPrimaryKeyName(%v) = %q, want %q", tt.platform, got, tt.want) + } + }) + } +} +``` + +### Test Helpers + +Use `t.Helper()` for functions that report failures on behalf of the caller: + +```go +func assertNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func assertEqual[T comparable](t *testing.T, got, want T) { + t.Helper() + if got != want { + t.Errorf("got %v, want %v", got, want) + } +} +``` + +### Test Fixtures and Golden Files + +For complex test data, use `testdata/` directories. For output comparison, use golden files: + +```go +func TestRenderReport(t *testing.T) { + got := renderReport(testInput) + + golden := filepath.Join("testdata", t.Name()+".golden") + if *update { + os.WriteFile(golden, []byte(got), 0644) + } + + want, _ := os.ReadFile(golden) + if got != string(want) { + t.Errorf("output mismatch; run with -update to refresh golden files") + } +} +``` + +### Fake Implementations Over Mocks + +Write simple fake structs that satisfy interfaces. They're more readable and more maintainable than mock framework magic: + +```go +type fakeAccountStore struct { + accounts []Account + err error +} + +func (f *fakeAccountStore) GetAccounts(ctx context.Context, tenant TenantID) ([]Account, error) { + return f.accounts, f.err +} + +// In test: +store := &fakeAccountStore{ + accounts: []Account{{ID: "1", Name: "Test"}}, +} +svc := NewReconciler(store, slog.Default()) +``` + +### Test Naming + +Pattern: `TestFunctionName_Scenario` using sub-tests for cases: + +```go +func TestReconciler_Run(t *testing.T) { + t.Run("empty accounts returns early", func(t *testing.T) { ... }) + t.Run("mismatched totals returns error", func(t *testing.T) { ... }) + t.Run("successful reconciliation", func(t *testing.T) { ... }) +} +``` + +### Parallel Tests + +Mark tests as parallel when they don't share mutable state: + +```go +func TestExpensiveComputation(t *testing.T) { + t.Parallel() + // ... +} +``` + +For table-driven tests, capture the loop variable and run subtests in parallel: + +```go +for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // ... + }) +} +``` + +--- + +## 15. Formatting & Layout + +### gofmt Is Non-Negotiable + +All code is formatted with `gofmt`. No exceptions, no arguments, no custom settings. Use `goimports` as a superset (handles import ordering too). Configure your editor to run it on save. + +### Line Length + +Go has no official line limit, but target ~100-120 characters for readability. Wrap function signatures and long expressions: + +```go +func (s *SyncService) ProcessBatch( + ctx context.Context, + tenant TenantID, + companyID CompanyID, + items []SyncItem, + opts ProcessOptions, +) (*BatchResult, error) { +``` + +### File Organization + +Within a file, order declarations: + +1. Package-level constants and variables +2. Types (structs, interfaces) +3. Constructor functions (`NewX`) +4. Methods grouped by receiver type +5. Package-level functions (helpers, utilities) + +### One Type Per File (Usually) + +Major types get their own file. Small related types (a struct and its constructor, an interface and a helper) can share a file. If a file exceeds ~400 lines, consider splitting. + +### Comments: Focus on "Why" + +Same as C#: comments explain *why*, not *what*. If the what/how isn't clear, improve the name: + +```go +// Bad: restating the code +// Check if rate is greater than zero +if rate > 0 { ... } + +// Good: explaining domain knowledge +// Fed data uses VEB (bolivar fuerte) instead of the current ISO code VES (bolivar soberano). +if strings.EqualFold(code, "VES") { + return "VEB" +} +``` + +### Package Comments + +Every package should have a doc comment in a `doc.go` or at the top of the primary file: + +```go +// Package reconcile provides tools for reconciling account data +// between external platforms and the internal ledger. +package reconcile +``` + +### Exported Function Documentation + +All exported functions, types, and methods have doc comments. Start with the name of the thing: + +```go +// GetAccounts returns all accounts for the given tenant and company. +// It returns an empty slice (not nil) if no accounts are found. +func (s *Store) GetAccounts(ctx context.Context, tenant TenantID, company CompanyID) ([]Account, error) { +``` + +--- + +## 16. Naming + +### Go Naming Conventions + +These are non-negotiable — they're enforced by the compiler and community norms: + +- **Exported** identifiers are `PascalCase`: `GetAccounts`, `SyncResult`, `ErrNotFound` +- **Unexported** identifiers are `camelCase`: `processItem`, `accountStore`, `defaultTimeout` +- **Acronyms** are all-caps: `ID`, `URL`, `HTTP`, `API` — not `Id`, `Url`, `Http` +- **Package names** are lowercase, single word when possible: `config`, `sync`, `ledger` — not `ledgerUtils`, `sync_helpers` +- **Interface names**: single-method interfaces use the `-er` suffix: `Reader`, `Writer`, `Closer`, `Fetcher`. Multi-method interfaces describe the role: `AccountStore`, `TokenProvider` + +### Receiver Names + +Use one or two letter abbreviations, consistent across all methods on a type. Never `self` or `this`: + +```go +func (r *Reconciler) Run(ctx context.Context) error { ... } +func (r *Reconciler) validate() error { ... } + +func (s *Store) GetAccounts(ctx context.Context) ([]Account, error) { ... } +``` + +### Variable Names + +Short names for short scopes, descriptive names for long scopes: + +```go +// Good: short scope, short name +for i, a := range accounts { ... } + +// Good: longer scope, descriptive name +var activeAccounts []Account +for _, account := range allAccounts { + if account.IsActive { + activeAccounts = append(activeAccounts, account) + } +} +``` + +### Don't Stutter + +Package names qualify their exports. Don't repeat the package name in the type name: + +```go +// Bad: config.ConfigOptions stutters +package config +type ConfigOptions struct { ... } + +// Good: config.Options reads naturally +package config +type Options struct { ... } +``` + +--- + +## 17. Dependency Management + +### Standard Library First + +Before reaching for a third-party package, check if the standard library covers it. Go's stdlib is unusually comprehensive. Common cases where teams reach for dependencies unnecessarily: + +- HTTP clients: `net/http` is excellent. You rarely need a wrapper. +- JSON: `encoding/json` covers most cases. Only reach for alternatives on hot paths with benchmarks. +- Logging: `log/slog` for CLI tools (see Section 11). `zap` is acceptable for web services. +- Testing: `testing` + table-driven tests covers 95% of needs. + +### Acceptable Common Dependencies + +These are fine to pull in without justification: + +- `github.com/spf13/cobra` — CLI framework (mandatory for all tools, see Section 6) +- `github.com/aws/aws-sdk-go-v2` — AWS API access +- `github.com/jackc/pgx/v5` — PostgreSQL driver +- `golang.org/x/sync/errgroup` — parallel goroutine management +- `go.uber.org/zap` — structured logging for web services (not CLIs) + +Everything else needs a reason. "It's popular" is not a reason. + +### Keeping Dependencies Updated + +Run `go get -u ./...` and `go mod tidy` regularly. Pin major versions in `go.mod`. Review changelogs for security patches. + +--- + +## 18. Zero Values and Nullability + +### Embrace the Zero Value + +Go's zero values (`""`, `0`, `false`, `nil` for pointers/slices/maps) are part of the type system. Design types so that the zero value is useful: + +```go +// Good: zero value is a valid, empty state +type BatchResult struct { + Processed int + Failed int + Errors []error // nil = no errors +} + +// r := BatchResult{} is already valid, means "nothing processed, no errors" +``` + +### Pointer Fields Mean "Optional" + +Use pointer fields when the zero value is meaningful and you need to distinguish "unset" from "zero": + +```go +type UpdateRequest struct { + Name *string // nil = don't update, "" = set to empty + Workers *int // nil = don't update, 0 = valid value + Active *bool // nil = don't update, false = valid value +} +``` + +### Validate at Boundaries + +Same philosophy as the C# guide: eliminate nil/zero concerns at the edges. Inside the domain, types should carry only valid state: + +```go +// Boundary: validate and reject +func (h *Handler) HandleSync(req *http.Request) error { + var input SyncRequest + if err := json.NewDecoder(req.Body).Decode(&input); err != nil { + return fmt.Errorf("invalid request body: %w", err) + } + if input.TenantID == "" { + return &ValidationError{Field: "tenant_id", Message: "required"} + } + // domain functions receive validated, non-zero data + return h.svc.Sync(req.Context(), TenantID(input.TenantID), CompanyID(input.CompanyID)) +} +``` + +### Empty Collections Over Nil in JSON + +When serializing for external consumers, initialize slices if `null` vs `[]` matters: + +```go +type Response struct { + Items []Item `json:"items"` +} + +// If Items might be nil, initialize before marshaling: +if resp.Items == nil { + resp.Items = []Item{} +} +``` + +--- + +## 19. AWS SDK Patterns + +### Use SDK v2 + +All new code uses `aws-sdk-go-v2`. Do not use v1 (`aws-sdk-go`). + +### SQS Long Polling + +```go +out, err := client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{ + QueueUrl: &queueURL, + WaitTimeSeconds: 20, + MaxNumberOfMessages: 10, +}) +``` + +### S3 Pagination + +Use the paginator helpers from SDK v2: + +```go +paginator := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{ + Bucket: &bucket, + Prefix: &prefix, +}) + +for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + return fmt.Errorf("listing objects: %w", err) + } + for _, obj := range page.Contents { + // process + } +} +``` + +### DynamoDB Batch Operations + +```go +// BatchWriteItem supports max 25 items +for _, batch := range Chunk(writeRequests, 25) { + _, err := client.BatchWriteItem(ctx, &dynamodb.BatchWriteItemInput{ + RequestItems: map[string][]types.WriteRequest{ + tableName: batch, + }, + }) + if err != nil { + return fmt.Errorf("batch write: %w", err) + } +} +``` + +--- + +## 20. Patterns to Maintain + +### Prefer Standard Library Over Hand-Rolled + +Same principle as the C# guide: check whether Go's stdlib already provides the functionality before writing a utility. In particular: + +- `slices` and `maps` packages replace many hand-rolled loops (Go 1.21+) +- `slog` replaces custom logging for CLI tools (Go 1.21+); `zap` remains appropriate for services +- `errors.Join` replaces custom multi-error types (Go 1.20+) +- `sync.OnceValue` replaces lazy initialization patterns (Go 1.21+) +- `http.NewServeMux` pattern matching replaces many router libraries (Go 1.22+) + +### decimal for Money + +Go's `float64` has the same problems as C#'s `double`. Use `shopspring/decimal` or a similar arbitrary-precision library for monetary calculations. Never `float64` for money: + +```go +import "github.com/shopspring/decimal" + +rate := decimal.NewFromString("0.0425") +monthly := rate.Div(decimal.NewFromInt(12)) +``` + +### time.Time, Not int64 + +Represent timestamps as `time.Time`, not Unix epoch integers. Convert at boundaries (JSON serialization, database storage), not in domain logic. + +### Performance Behind Good Names + +Same as C#: a function named `GetExchangeRate` can use `unsafe.Pointer` arithmetic internally if profiling demands it. The caller sees a clean API. Optimize hot paths, not cold paths. Profile before optimizing. + +### Composition Over Inheritance + +Go doesn't have inheritance. Use embedding for shared structure, interfaces for shared behavior: + +```go +// Embedding: shared fields +type BaseJob struct { + TenantID TenantID + CompanyID CompanyID + CreatedAt time.Time +} + +type SyncJob struct { + BaseJob + Platform PlatformType + Priority int +} + +// Interface: shared behavior +type Processor interface { + Process(ctx context.Context, job Job) error +} +``` diff --git a/shared/adf/convert_test.go b/shared/adf/convert_test.go index a804635..f4631e2 100644 --- a/shared/adf/convert_test.go +++ b/shared/adf/convert_test.go @@ -2,30 +2,30 @@ package adf import ( "encoding/json" + "fmt" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestToJSON_Paragraph(t *testing.T) { input := "Hello world" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, "doc", doc.Type) - assert.Equal(t, 1, doc.Version) - require.Len(t, doc.Content, 1) + testutil.Equal(t, doc.Type, "doc") + testutil.Equal(t, doc.Version, 1) + testutil.RequireEqual(t, len(doc.Content), 1) para := doc.Content[0] - assert.Equal(t, "paragraph", para.Type) - require.Len(t, para.Content, 1) - assert.Equal(t, "text", para.Content[0].Type) - assert.Equal(t, "Hello world", para.Content[0].Text) + testutil.Equal(t, para.Type, "paragraph") + testutil.RequireEqual(t, len(para.Content), 1) + testutil.Equal(t, para.Content[0].Type, "text") + testutil.Equal(t, para.Content[0].Text, "Hello world") } func TestToJSON_Headings(t *testing.T) { @@ -46,18 +46,18 @@ func TestToJSON_Headings(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToJSON([]byte(tt.markdown)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) heading := doc.Content[0] - assert.Equal(t, "heading", heading.Type) - assert.EqualValues(t, tt.level, heading.Attrs["level"]) - require.Len(t, heading.Content, 1) - assert.Equal(t, tt.text, heading.Content[0].Text) + testutil.Equal(t, heading.Type, "heading") + testutil.Equal(t, heading.Attrs["level"], float64(tt.level)) + testutil.RequireEqual(t, len(heading.Content), 1) + testutil.Equal(t, heading.Content[0].Text, tt.text) }) } } @@ -77,15 +77,15 @@ func TestToJSON_Formatting(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToJSON([]byte(tt.markdown)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) para := doc.Content[0] - assert.Equal(t, "paragraph", para.Type) + testutil.Equal(t, para.Type, "paragraph") var foundMark bool for _, node := range para.Content { @@ -98,7 +98,7 @@ func TestToJSON_Formatting(t *testing.T) { } } } - assert.True(t, foundMark, "expected to find mark %s", tt.mark) + testutil.True(t, foundMark, fmt.Sprintf("expected to find mark %s", tt.mark)) }) } } @@ -106,13 +106,13 @@ func TestToJSON_Formatting(t *testing.T) { func TestToJSON_Links(t *testing.T) { input := "[Example](https://example.com)" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) para := doc.Content[0] var foundLink bool @@ -120,53 +120,53 @@ func TestToJSON_Links(t *testing.T) { for _, mark := range node.Marks { if mark.Type == "link" { foundLink = true - assert.Equal(t, "https://example.com", mark.Attrs["href"]) - assert.Equal(t, "Example", node.Text) + testutil.Equal(t, mark.Attrs["href"], "https://example.com") + testutil.Equal(t, node.Text, "Example") } } } - assert.True(t, foundLink, "expected to find link mark") + testutil.True(t, foundLink, "expected to find link mark") } func TestToJSON_BulletList(t *testing.T) { input := "- Item one\n- Item two\n- Item three" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) list := doc.Content[0] - assert.Equal(t, "bulletList", list.Type) - assert.Len(t, list.Content, 3) + testutil.Equal(t, list.Type, "bulletList") + testutil.Len(t, list.Content, 3) for i, item := range list.Content { - assert.Equal(t, "listItem", item.Type) - require.Len(t, item.Content, 1) + testutil.Equal(t, item.Type, "listItem") + testutil.RequireEqual(t, len(item.Content), 1) para := item.Content[0] - assert.Equal(t, "paragraph", para.Type) + testutil.Equal(t, para.Type, "paragraph") expected := []string{"Item one", "Item two", "Item three"}[i] - require.Len(t, para.Content, 1) - assert.Equal(t, expected, para.Content[0].Text) + testutil.RequireEqual(t, len(para.Content), 1) + testutil.Equal(t, para.Content[0].Text, expected) } } func TestToJSON_OrderedList(t *testing.T) { input := "1. First\n2. Second\n3. Third" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) list := doc.Content[0] - assert.Equal(t, "orderedList", list.Type) - assert.EqualValues(t, 1, list.Attrs["order"]) - assert.Len(t, list.Content, 3) + testutil.Equal(t, list.Type, "orderedList") + testutil.Equal(t, list.Attrs["order"], float64(1)) + testutil.Len(t, list.Content, 3) } func TestToJSON_CodeBlock(t *testing.T) { @@ -199,22 +199,22 @@ func TestToJSON_CodeBlock(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToJSON([]byte(tt.markdown)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) block := doc.Content[0] - assert.Equal(t, "codeBlock", block.Type) + testutil.Equal(t, block.Type, "codeBlock") if tt.language != "" { - assert.Equal(t, tt.language, block.Attrs["language"]) + testutil.Equal(t, block.Attrs["language"], tt.language) } - require.Len(t, block.Content, 1) - assert.Equal(t, tt.code, block.Content[0].Text) + testutil.RequireEqual(t, len(block.Content), 1) + testutil.Equal(t, block.Content[0].Text, tt.code) }) } } @@ -222,108 +222,108 @@ func TestToJSON_CodeBlock(t *testing.T) { func TestToJSON_Blockquote(t *testing.T) { input := "> This is a quote" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) quote := doc.Content[0] - assert.Equal(t, "blockquote", quote.Type) - require.Len(t, quote.Content, 1) - assert.Equal(t, "paragraph", quote.Content[0].Type) + testutil.Equal(t, quote.Type, "blockquote") + testutil.RequireEqual(t, len(quote.Content), 1) + testutil.Equal(t, quote.Content[0].Type, "paragraph") } func TestToJSON_HorizontalRule(t *testing.T) { input := "Above\n\n---\n\nBelow" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Len(t, doc.Content, 3) - assert.Equal(t, "paragraph", doc.Content[0].Type) - assert.Equal(t, "rule", doc.Content[1].Type) - assert.Equal(t, "paragraph", doc.Content[2].Type) + testutil.Len(t, doc.Content, 3) + testutil.Equal(t, doc.Content[0].Type, "paragraph") + testutil.Equal(t, doc.Content[1].Type, "rule") + testutil.Equal(t, doc.Content[2].Type, "paragraph") } func TestToJSON_Table(t *testing.T) { input := "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) table := doc.Content[0] - assert.Equal(t, "table", table.Type) - assert.Len(t, table.Content, 2) + testutil.Equal(t, table.Type, "table") + testutil.Len(t, table.Content, 2) headerRow := table.Content[0] - assert.Equal(t, "tableRow", headerRow.Type) - assert.Len(t, headerRow.Content, 2) - assert.Equal(t, "tableHeader", headerRow.Content[0].Type) + testutil.Equal(t, headerRow.Type, "tableRow") + testutil.Len(t, headerRow.Content, 2) + testutil.Equal(t, headerRow.Content[0].Type, "tableHeader") dataRow := table.Content[1] - assert.Equal(t, "tableRow", dataRow.Type) - assert.Len(t, dataRow.Content, 2) - assert.Equal(t, "tableCell", dataRow.Content[0].Type) + testutil.Equal(t, dataRow.Type, "tableRow") + testutil.Len(t, dataRow.Content, 2) + testutil.Equal(t, dataRow.Content[0].Type, "tableCell") } func TestToJSON_EmptyInput(t *testing.T) { result, err := ToJSON([]byte("")) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, "doc", doc.Type) - assert.Equal(t, 1, doc.Version) - assert.Empty(t, doc.Content) + testutil.Equal(t, doc.Type, "doc") + testutil.Equal(t, doc.Version, 1) + testutil.Empty(t, doc.Content) } func TestToJSON_NestedList(t *testing.T) { input := "- Item one\n - Nested one\n - Nested two\n- Item two" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) list := doc.Content[0] - assert.Equal(t, "bulletList", list.Type) + testutil.Equal(t, list.Type, "bulletList") firstItem := list.Content[0] - assert.Equal(t, "listItem", firstItem.Type) + testutil.Equal(t, firstItem.Type, "listItem") var foundNestedList bool for _, child := range firstItem.Content { if child.Type == "bulletList" { foundNestedList = true - assert.Len(t, child.Content, 2) + testutil.Len(t, child.Content, 2) } } - assert.True(t, foundNestedList, "expected nested bullet list") + testutil.True(t, foundNestedList, "expected nested bullet list") } func TestToJSON_BoldAndItalicCombined(t *testing.T) { input := "***bold and italic***" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) para := doc.Content[0] var foundStrong, foundEm bool @@ -337,8 +337,8 @@ func TestToJSON_BoldAndItalicCombined(t *testing.T) { } } } - assert.True(t, foundStrong, "expected strong mark") - assert.True(t, foundEm, "expected em mark") + testutil.True(t, foundStrong, "expected strong mark") + testutil.True(t, foundEm, "expected em mark") } func TestToJSON_OutputIsValidJSON(t *testing.T) { @@ -352,79 +352,79 @@ func TestToJSON_OutputIsValidJSON(t *testing.T) { for _, input := range inputs { result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var parsed map[string]interface{} err = json.Unmarshal([]byte(result), &parsed) - require.NoError(t, err, "Output should be valid JSON for input: %s", input) + testutil.RequireNoError(t, err) - assert.Equal(t, "doc", parsed["type"]) - assert.EqualValues(t, 1, parsed["version"]) + testutil.Equal(t, parsed["type"], "doc") + testutil.Equal(t, parsed["version"], float64(1)) } } func TestToJSON_Images_AltText(t *testing.T) { input := "![Alt text](https://example.com/image.png)" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) para := doc.Content[0] - assert.Equal(t, "paragraph", para.Type) - require.Len(t, para.Content, 1) - assert.Equal(t, "Alt text", para.Content[0].Text) + testutil.Equal(t, para.Type, "paragraph") + testutil.RequireEqual(t, len(para.Content), 1) + testutil.Equal(t, para.Content[0].Text, "Alt text") } func TestToJSON_WhitespaceInCodeBlock(t *testing.T) { input := "```\n indented code\n more indented\n```" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) block := doc.Content[0] - assert.Equal(t, "codeBlock", block.Type) - require.Len(t, block.Content, 1) + testutil.Equal(t, block.Type, "codeBlock") + testutil.RequireEqual(t, len(block.Content), 1) text := block.Content[0].Text - assert.Contains(t, text, " indented") - assert.Contains(t, text, " more indented") + testutil.Contains(t, text, " indented") + testutil.Contains(t, text, " more indented") } func TestToJSON_NestedBlockquote(t *testing.T) { input := "> Quote with **bold** text\n>\n> And a list:\n> - Item 1\n> - Item 2" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) quote := doc.Content[0] - assert.Equal(t, "blockquote", quote.Type) - assert.True(t, len(quote.Content) > 0, "blockquote should have content") + testutil.Equal(t, quote.Type, "blockquote") + testutil.True(t, len(quote.Content) > 0, "blockquote should have content") } func TestToJSON_HardLineBreak(t *testing.T) { input := "Line one \nLine two" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) para := doc.Content[0] - assert.Equal(t, "paragraph", para.Type) + testutil.Equal(t, para.Type, "paragraph") var foundBreak bool for _, node := range para.Content { @@ -433,19 +433,19 @@ func TestToJSON_HardLineBreak(t *testing.T) { break } } - assert.True(t, foundBreak, "expected hardBreak node") + testutil.True(t, foundBreak, "expected hardBreak node") } func TestToJSON_InlineCodePreservesContent(t *testing.T) { input := "Use `fmt.Println()` to print" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) para := doc.Content[0] var foundCode bool @@ -453,73 +453,73 @@ func TestToJSON_InlineCodePreservesContent(t *testing.T) { for _, mark := range node.Marks { if mark.Type == "code" { foundCode = true - assert.Equal(t, "fmt.Println()", node.Text) + testutil.Equal(t, node.Text, "fmt.Println()") } } } - assert.True(t, foundCode, "expected code mark") + testutil.True(t, foundCode, "expected code mark") } func TestToDocument_Empty(t *testing.T) { doc := ToDocument("") - assert.Nil(t, doc) + testutil.Nil(t, doc) } func TestToDocument_PlainText(t *testing.T) { doc := ToDocument("Hello world") - require.NotNil(t, doc) - assert.Equal(t, "doc", doc.Type) - assert.Equal(t, 1, doc.Version) - require.Len(t, doc.Content, 1) - assert.Equal(t, "paragraph", doc.Content[0].Type) - require.Len(t, doc.Content[0].Content, 1) - assert.Equal(t, "Hello world", doc.Content[0].Content[0].Text) + testutil.NotNil(t, doc) + testutil.Equal(t, doc.Type, "doc") + testutil.Equal(t, doc.Version, 1) + testutil.RequireEqual(t, len(doc.Content), 1) + testutil.Equal(t, doc.Content[0].Type, "paragraph") + testutil.RequireEqual(t, len(doc.Content[0].Content), 1) + testutil.Equal(t, doc.Content[0].Content[0].Text, "Hello world") } func TestToDocument_ToPlainText(t *testing.T) { doc := ToDocument("# Title\n\nSome text\n\n- Item 1\n- Item 2") - require.NotNil(t, doc) + testutil.NotNil(t, doc) text := doc.ToPlainText() - assert.Contains(t, text, "Title") - assert.Contains(t, text, "Some text") - assert.Contains(t, text, "Item 1") - assert.Contains(t, text, "Item 2") + testutil.Contains(t, text, "Title") + testutil.Contains(t, text, "Some text") + testutil.Contains(t, text, "Item 1") + testutil.Contains(t, text, "Item 2") } func TestToPlainText_Nil(t *testing.T) { var doc *Document - assert.Equal(t, "", doc.ToPlainText()) + testutil.Equal(t, doc.ToPlainText(), "") } func TestToJSON_IndentedCodeBlock(t *testing.T) { input := " code line one\n code line two" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) block := doc.Content[0] - assert.Equal(t, "codeBlock", block.Type) - assert.Nil(t, block.Attrs, "indented code blocks should have no language attr") - require.Len(t, block.Content, 1) - assert.Contains(t, block.Content[0].Text, "code line one") - assert.Contains(t, block.Content[0].Text, "code line two") + testutil.Equal(t, block.Type, "codeBlock") + testutil.Nil(t, block.Attrs) + testutil.RequireEqual(t, len(block.Content), 1) + testutil.Contains(t, block.Content[0].Text, "code line one") + testutil.Contains(t, block.Content[0].Text, "code line two") } func TestToJSON_AutoLink(t *testing.T) { input := "Visit for info" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) para := doc.Content[0] var foundAutoLink bool @@ -527,24 +527,24 @@ func TestToJSON_AutoLink(t *testing.T) { for _, mark := range node.Marks { if mark.Type == "link" { foundAutoLink = true - assert.Equal(t, "https://example.com", mark.Attrs["href"]) - assert.Equal(t, "https://example.com", node.Text) + testutil.Equal(t, mark.Attrs["href"], "https://example.com") + testutil.Equal(t, node.Text, "https://example.com") } } } - assert.True(t, foundAutoLink, "expected to find auto-linked URL") + testutil.True(t, foundAutoLink, "expected to find auto-linked URL") } func TestToJSON_RawHTMLDropped(t *testing.T) { input := "Before raw after" result, err := ToJSON([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc Document err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.RequireEqual(t, len(doc.Content), 1) para := doc.Content[0] // Raw HTML should be dropped; surrounding text should remain @@ -552,9 +552,9 @@ func TestToJSON_RawHTMLDropped(t *testing.T) { for _, node := range para.Content { allText += node.Text } - assert.Contains(t, allText, "Before") - assert.Contains(t, allText, "after") - assert.NotContains(t, allText, "") + testutil.Contains(t, allText, "Before") + testutil.Contains(t, allText, "after") + testutil.NotContains(t, allText, "") } func TestSplitLines(t *testing.T) { @@ -573,7 +573,7 @@ func TestSplitLines(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := splitLines(tt.input) - assert.Equal(t, tt.want, got) + testutil.Equal(t, got, tt.want) }) } } @@ -593,7 +593,7 @@ func TestToPlainText_CodeBlock(t *testing.T) { } text := doc.ToPlainText() - assert.Contains(t, text, "fmt.Println()") + testutil.Contains(t, text, "fmt.Println()") } func TestToPlainText_Blockquote(t *testing.T) { @@ -616,7 +616,7 @@ func TestToPlainText_Blockquote(t *testing.T) { } text := doc.ToPlainText() - assert.Contains(t, text, "> Quoted") + testutil.Contains(t, text, "> Quoted") } func TestToPlainText_Rule(t *testing.T) { @@ -629,7 +629,7 @@ func TestToPlainText_Rule(t *testing.T) { } text := doc.ToPlainText() - assert.Contains(t, text, "---") + testutil.Contains(t, text, "---") } func TestToPlainText_UnknownNodeType(t *testing.T) { @@ -642,7 +642,7 @@ func TestToPlainText_UnknownNodeType(t *testing.T) { } text := doc.ToPlainText() - assert.Contains(t, text, "fallback text") + testutil.Contains(t, text, "fallback text") } func TestToPlainText_UnknownNodeWithChildren(t *testing.T) { @@ -665,5 +665,5 @@ func TestToPlainText_UnknownNodeWithChildren(t *testing.T) { } text := doc.ToPlainText() - assert.Contains(t, text, "inner") + testutil.Contains(t, text, "inner") } diff --git a/shared/go.mod b/shared/go.mod index b15a915..5535369 100644 --- a/shared/go.mod +++ b/shared/go.mod @@ -4,15 +4,11 @@ go 1.24.0 require ( github.com/fatih/color v1.18.0 - github.com/stretchr/testify v1.11.1 github.com/yuin/goldmark v1.7.16 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.38.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/shared/go.sum b/shared/go.sum index 19c2733..bd543e7 100644 --- a/shared/go.sum +++ b/shared/go.sum @@ -1,5 +1,3 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -7,17 +5,9 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/shared/prompt/confirm_test.go b/shared/prompt/confirm_test.go index 89a5f9a..918ac99 100644 --- a/shared/prompt/confirm_test.go +++ b/shared/prompt/confirm_test.go @@ -4,8 +4,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestConfirm(t *testing.T) { @@ -56,11 +55,11 @@ func TestConfirm(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := Confirm(strings.NewReader(tt.input)) if tt.wantErr { - require.Error(t, err) + testutil.RequireError(t, err) return } - require.NoError(t, err) - assert.Equal(t, tt.want, got) + testutil.RequireNoError(t, err) + testutil.Equal(t, got, tt.want) }) } } @@ -103,11 +102,11 @@ func TestConfirmOrForce(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := ConfirmOrForce(tt.force, strings.NewReader(tt.input)) if tt.wantErr { - require.Error(t, err) + testutil.RequireError(t, err) return } - require.NoError(t, err) - assert.Equal(t, tt.want, got) + testutil.RequireNoError(t, err) + testutil.Equal(t, got, tt.want) }) } } diff --git a/shared/testutil/assert.go b/shared/testutil/assert.go new file mode 100644 index 0000000..26ddd3a --- /dev/null +++ b/shared/testutil/assert.go @@ -0,0 +1,229 @@ +// Package testutil provides lightweight test assertion helpers. +// +// All helpers call t.Helper() so test failures report the caller's line number. +// "Require" variants call t.Fatal (stop the test); plain variants call t.Error +// (mark failed but continue). +package testutil + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +// Equal checks that got and want are deeply equal. +func Equal(t *testing.T, got, want interface{}) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, want %v", got, want) + } +} + +// RequireEqual checks that got and want are deeply equal, stopping the test on failure. +func RequireEqual(t *testing.T, got, want interface{}) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } +} + +// NoError checks that err is nil. +func NoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} + +// RequireNoError checks that err is nil, stopping the test on failure. +func RequireNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// Error checks that err is not nil. +func Error(t *testing.T, err error) { + t.Helper() + if err == nil { + t.Errorf("expected error, got nil") + } +} + +// RequireError checks that err is not nil, stopping the test on failure. +func RequireError(t *testing.T, err error) { + t.Helper() + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +// Contains checks that s contains substr. +func Contains(t *testing.T, s, substr string) { + t.Helper() + if !strings.Contains(s, substr) { + t.Errorf("string %q does not contain %q", s, substr) + } +} + +// True checks that the condition is true. +func True(t *testing.T, condition bool, msgAndArgs ...interface{}) { + t.Helper() + if !condition { + if len(msgAndArgs) > 0 { + t.Errorf("expected true: %s", fmt.Sprint(msgAndArgs...)) + } else { + t.Errorf("expected true, got false") + } + } +} + +// False checks that the condition is false. +func False(t *testing.T, condition bool, msgAndArgs ...interface{}) { + t.Helper() + if condition { + if len(msgAndArgs) > 0 { + t.Errorf("expected false: %s", fmt.Sprint(msgAndArgs...)) + } else { + t.Errorf("expected false, got true") + } + } +} + +// Nil checks that v is nil. +func Nil(t *testing.T, v interface{}) { + t.Helper() + if v == nil { + return + } + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface || + rv.Kind() == reflect.Map || rv.Kind() == reflect.Slice || + rv.Kind() == reflect.Chan || rv.Kind() == reflect.Func { + if rv.IsNil() { + return + } + } + t.Errorf("expected nil, got %v", v) +} + +// NotNil checks that v is not nil. +func NotNil(t *testing.T, v interface{}) { + t.Helper() + if v == nil { + t.Errorf("expected not nil, got nil") + return + } + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface || + rv.Kind() == reflect.Map || rv.Kind() == reflect.Slice || + rv.Kind() == reflect.Chan || rv.Kind() == reflect.Func { + if rv.IsNil() { + t.Errorf("expected not nil, got nil") + } + } +} + +// Len checks that the length of v equals expected. +// v must be a string, slice, array, map, or channel. +func Len(t *testing.T, v interface{}, expected int) { + t.Helper() + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.String, reflect.Slice, reflect.Array, reflect.Map, reflect.Chan: + if rv.Len() != expected { + t.Errorf("expected length %d, got %d", expected, rv.Len()) + } + default: + t.Errorf("Len called on unsupported type %T", v) + } +} + +// Empty checks that v has length 0. +func Empty(t *testing.T, v interface{}) { + t.Helper() + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.String, reflect.Slice, reflect.Array, reflect.Map, reflect.Chan: + if rv.Len() != 0 { + t.Errorf("expected empty, got length %d", rv.Len()) + } + default: + t.Errorf("Empty called on unsupported type %T", v) + } +} + +// NotEmpty checks that v has length > 0. +func NotEmpty(t *testing.T, v interface{}) { + t.Helper() + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.String, reflect.Slice, reflect.Array, reflect.Map, reflect.Chan: + if rv.Len() == 0 { + t.Errorf("expected not empty") + } + default: + t.Errorf("NotEmpty called on unsupported type %T", v) + } +} + +// Greater checks that a > b (both must be comparable numeric types or strings). +func Greater(t *testing.T, a, b int) { + t.Helper() + if a <= b { + t.Errorf("expected %d > %d", a, b) + } +} + +// GreaterOrEqual checks that a >= b. +func GreaterOrEqual(t *testing.T, a, b int) { + t.Helper() + if a < b { + t.Errorf("expected %d >= %d", a, b) + } +} + +// NotContains checks that s does not contain substr. +func NotContains(t *testing.T, s, substr string) { + t.Helper() + if strings.Contains(s, substr) { + t.Errorf("string %q should not contain %q", s, substr) + } +} + +// HasPrefix checks that s starts with prefix. +func HasPrefix(t *testing.T, s, prefix string) { + t.Helper() + if !strings.HasPrefix(s, prefix) { + t.Errorf("string %q does not start with %q", s, prefix) + } +} + +// HasSuffix checks that s ends with suffix. +func HasSuffix(t *testing.T, s, suffix string) { + t.Helper() + if !strings.HasSuffix(s, suffix) { + t.Errorf("string %q does not end with %q", s, suffix) + } +} + +// NotEqual checks that got and want are not deeply equal. +func NotEqual(t *testing.T, got, want interface{}) { + t.Helper() + if reflect.DeepEqual(got, want) { + t.Errorf("expected values to differ, both are %v", got) + } +} + +// ErrorContains checks that err is not nil and its message contains substr. +func ErrorContains(t *testing.T, err error, substr string) { + t.Helper() + if err == nil { + t.Fatalf("expected error containing %q, got nil", substr) + } + if !strings.Contains(err.Error(), substr) { + t.Errorf("error %q does not contain %q", err.Error(), substr) + } +} diff --git a/tools/cfl/CLAUDE.md b/tools/cfl/CLAUDE.md index ec649b9..c272e13 100644 --- a/tools/cfl/CLAUDE.md +++ b/tools/cfl/CLAUDE.md @@ -128,7 +128,7 @@ type pageAPI interface { - `*_test.go` next to implementation - `testdata/` for JSON fixtures - Table-driven tests with `t.Run()` -- Use `github.com/stretchr/testify/assert` and `require` +- Use `shared/testutil` for assertions (no testify) ### Integration Tests After significant code changes, run through the manual integration test suite in [integration-tests.md](integration-tests.md). These tests verify real-world behavior against a live Confluence instance and catch edge cases that unit tests miss. diff --git a/tools/cfl/api/attachments_test.go b/tools/cfl/api/attachments_test.go index 0c6eeb2..8442508 100644 --- a/tools/cfl/api/attachments_test.go +++ b/tools/cfl/api/attachments_test.go @@ -6,16 +6,15 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestClient_ListAttachments(t *testing.T) { testData := loadTestData(t, "attachments.json") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/pages/98765/attachments", r.URL.Path) - assert.Equal(t, "GET", r.Method) + testutil.Equal(t, "/api/v2/pages/98765/attachments", r.URL.Path) + testutil.Equal(t, "GET", r.Method) w.WriteHeader(http.StatusOK) _, _ = w.Write(testData) @@ -25,21 +24,21 @@ func TestClient_ListAttachments(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") result, err := client.ListAttachments(context.Background(), "98765", nil) - require.NoError(t, err) - assert.Len(t, result.Results, 2) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Results, 2) // Check first attachment att := result.Results[0] - assert.Equal(t, "att111", att.ID) - assert.Equal(t, "screenshot.png", att.Title) - assert.Equal(t, "image/png", att.MediaType) - assert.Equal(t, int64(245678), att.FileSize) + testutil.Equal(t, "att111", att.ID) + testutil.Equal(t, "screenshot.png", att.Title) + testutil.Equal(t, "image/png", att.MediaType) + testutil.Equal(t, int64(245678), att.FileSize) } func TestClient_ListAttachments_WithOptions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "50", r.URL.Query().Get("limit")) - assert.Equal(t, "image/png", r.URL.Query().Get("mediaType")) + testutil.Equal(t, "50", r.URL.Query().Get("limit")) + testutil.Equal(t, "image/png", r.URL.Query().Get("mediaType")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": []}`)) @@ -52,13 +51,13 @@ func TestClient_ListAttachments_WithOptions(t *testing.T) { MediaType: "image/png", } _, err := client.ListAttachments(context.Background(), "98765", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_GetAttachment(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/attachments/att111", r.URL.Path) - assert.Equal(t, "GET", r.Method) + testutil.Equal(t, "/api/v2/attachments/att111", r.URL.Path) + testutil.Equal(t, "GET", r.Method) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -74,10 +73,10 @@ func TestClient_GetAttachment(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") att, err := client.GetAttachment(context.Background(), "att111") - require.NoError(t, err) - assert.Equal(t, "att111", att.ID) - assert.Equal(t, "screenshot.png", att.Title) - assert.Equal(t, int64(245678), att.FileSize) + testutil.RequireNoError(t, err) + testutil.Equal(t, "att111", att.ID) + testutil.Equal(t, "screenshot.png", att.Title) + testutil.Equal(t, int64(245678), att.FileSize) } func TestClient_DownloadAttachment(t *testing.T) { @@ -111,15 +110,15 @@ func TestClient_DownloadAttachment(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") reader, err := client.DownloadAttachment(context.Background(), "att111") - require.NoError(t, err) + testutil.RequireNoError(t, err) defer func() { _ = reader.Close() }() - assert.True(t, downloadCalled, "should have called download link") + testutil.True(t, downloadCalled, "should have called download link") // Read and verify content buf := make([]byte, 100) n, _ := reader.Read(buf) - assert.Equal(t, fileContent, buf[:n]) + testutil.Equal(t, fileContent, buf[:n]) } func TestClient_DownloadAttachment_NoDownloadLink(t *testing.T) { @@ -131,21 +130,21 @@ func TestClient_DownloadAttachment_NoDownloadLink(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") _, err := client.DownloadAttachment(context.Background(), "att123") - require.Error(t, err) - assert.Contains(t, err.Error(), "no download link") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "no download link") } func TestClient_DeleteAttachment(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/attachments/att111", r.URL.Path) - assert.Equal(t, "DELETE", r.Method) + testutil.Equal(t, "/api/v2/attachments/att111", r.URL.Path) + testutil.Equal(t, "DELETE", r.Method) w.WriteHeader(http.StatusNoContent) })) defer server.Close() client := NewClient(server.URL, "user@example.com", "token") err := client.DeleteAttachment(context.Background(), "att111") - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_DeleteAttachment_NotFound(t *testing.T) { @@ -157,5 +156,5 @@ func TestClient_DeleteAttachment_NotFound(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") err := client.DeleteAttachment(context.Background(), "invalid") - require.Error(t, err) + testutil.RequireError(t, err) } diff --git a/tools/cfl/api/client_test.go b/tools/cfl/api/client_test.go index adadca3..1552918 100644 --- a/tools/cfl/api/client_test.go +++ b/tools/cfl/api/client_test.go @@ -8,16 +8,15 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestNewClient(t *testing.T) { client := NewClient("https://example.atlassian.net/wiki", "user@example.com", "token123") - assert.NotNil(t, client) - assert.Equal(t, "https://example.atlassian.net/wiki", client.GetBaseURL()) - assert.Contains(t, client.GetAuthHeader(), "Basic ") + testutil.NotNil(t, client) + testutil.Equal(t, "https://example.atlassian.net/wiki", client.GetBaseURL()) + testutil.Contains(t, client.GetAuthHeader(), "Basic ") } func TestClient_AuthHeader(t *testing.T) { @@ -32,14 +31,14 @@ func TestClient_AuthHeader(t *testing.T) { client := NewClient(server.URL, "user@example.com", "mytoken") _, err := client.Get(context.Background(), "/test") - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify Basic auth header - require.True(t, strings.HasPrefix(capturedAuth, "Basic ")) + testutil.True(t, strings.HasPrefix(capturedAuth, "Basic ")) encoded := strings.TrimPrefix(capturedAuth, "Basic ") decoded, err := base64.StdEncoding.DecodeString(encoded) - require.NoError(t, err) - assert.Equal(t, "user@example.com:mytoken", string(decoded)) + testutil.RequireNoError(t, err) + testutil.Equal(t, "user@example.com:mytoken", string(decoded)) } func TestClient_Headers(t *testing.T) { @@ -54,10 +53,10 @@ func TestClient_Headers(t *testing.T) { client := NewClient(server.URL, "user@example.com", "mytoken") _, err := client.Get(context.Background(), "/test") - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, "application/json", capturedHeaders.Get("Accept")) - assert.Equal(t, "application/json", capturedHeaders.Get("Content-Type")) + testutil.Equal(t, "application/json", capturedHeaders.Get("Accept")) + testutil.Equal(t, "application/json", capturedHeaders.Get("Content-Type")) } func TestClient_ErrorResponse(t *testing.T) { @@ -110,8 +109,8 @@ func TestClient_ErrorResponse(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") _, err := client.Get(context.Background(), "/test") - require.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedErrMsg) + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), tt.expectedErrMsg) }) } } @@ -129,7 +128,7 @@ func TestClient_ContextCancellation(t *testing.T) { cancel() // Cancel immediately _, err := client.Get(ctx, "/test") - require.Error(t, err) + testutil.RequireError(t, err) } func TestClient_URLConstruction(t *testing.T) { @@ -154,7 +153,7 @@ func TestClient_URLConstruction(t *testing.T) { for _, tt := range tests { _, err := client.Get(context.Background(), tt.inputPath) - require.NoError(t, err) - assert.Equal(t, tt.expectedPath, capturedPath) + testutil.RequireNoError(t, err) + testutil.Equal(t, tt.expectedPath, capturedPath) } } diff --git a/tools/cfl/api/pages_test.go b/tools/cfl/api/pages_test.go index 3192120..cebffe3 100644 --- a/tools/cfl/api/pages_test.go +++ b/tools/cfl/api/pages_test.go @@ -8,17 +8,16 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestClient_ListPages(t *testing.T) { testData := loadTestData(t, "pages.json") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/spaces/123456/pages", r.URL.Path) - assert.Equal(t, "GET", r.Method) - assert.Equal(t, "25", r.URL.Query().Get("limit")) + testutil.Equal(t, "/api/v2/spaces/123456/pages", r.URL.Path) + testutil.Equal(t, "GET", r.Method) + testutil.Equal(t, "25", r.URL.Query().Get("limit")) w.WriteHeader(http.StatusOK) _, _ = w.Write(testData) @@ -28,22 +27,22 @@ func TestClient_ListPages(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") result, err := client.ListPages(context.Background(), "123456", nil) - require.NoError(t, err) - assert.Len(t, result.Results, 2) - assert.True(t, result.HasMore()) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Results, 2) + testutil.True(t, result.HasMore()) // Check first page page := result.Results[0] - assert.Equal(t, "98765", page.ID) - assert.Equal(t, "Getting Started Guide", page.Title) - assert.Equal(t, "123456", page.SpaceID) + testutil.Equal(t, "98765", page.ID) + testutil.Equal(t, "Getting Started Guide", page.Title) + testutil.Equal(t, "123456", page.SpaceID) } func TestClient_ListPages_WithOptions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "50", r.URL.Query().Get("limit")) - assert.Equal(t, "current", r.URL.Query().Get("status")) - assert.Equal(t, "title", r.URL.Query().Get("sort")) + testutil.Equal(t, "50", r.URL.Query().Get("limit")) + testutil.Equal(t, "current", r.URL.Query().Get("status")) + testutil.Equal(t, "title", r.URL.Query().Get("sort")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": []}`)) @@ -57,15 +56,15 @@ func TestClient_ListPages_WithOptions(t *testing.T) { Sort: "title", } _, err := client.ListPages(context.Background(), "123456", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_GetPage(t *testing.T) { testData := loadTestData(t, "page.json") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/pages/98765", r.URL.Path) - assert.Equal(t, "GET", r.Method) + testutil.Equal(t, "/api/v2/pages/98765", r.URL.Path) + testutil.Equal(t, "GET", r.Method) w.WriteHeader(http.StatusOK) _, _ = w.Write(testData) @@ -75,19 +74,19 @@ func TestClient_GetPage(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") page, err := client.GetPage(context.Background(), "98765", nil) - require.NoError(t, err) - assert.Equal(t, "98765", page.ID) - assert.Equal(t, "Getting Started Guide", page.Title) - assert.Equal(t, "123456", page.SpaceID) - assert.Equal(t, 5, page.Version.Number) - assert.NotNil(t, page.Body) - assert.NotNil(t, page.Body.Storage) - assert.Contains(t, page.Body.Storage.Value, "

Getting Started

") + testutil.RequireNoError(t, err) + testutil.Equal(t, "98765", page.ID) + testutil.Equal(t, "Getting Started Guide", page.Title) + testutil.Equal(t, "123456", page.SpaceID) + testutil.Equal(t, 5, page.Version.Number) + testutil.NotNil(t, page.Body) + testutil.NotNil(t, page.Body.Storage) + testutil.Contains(t, page.Body.Storage.Value, "

Getting Started

") } func TestClient_GetPage_WithBodyFormat(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "storage", r.URL.Query().Get("body-format")) + testutil.Equal(t, "storage", r.URL.Query().Get("body-format")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"id": "98765", "title": "Test"}`)) @@ -97,26 +96,26 @@ func TestClient_GetPage_WithBodyFormat(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") opts := &GetPageOptions{BodyFormat: "storage"} _, err := client.GetPage(context.Background(), "98765", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_CreatePage(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/pages", r.URL.Path) - assert.Equal(t, "POST", r.Method) + testutil.Equal(t, "/api/v2/pages", r.URL.Path) + testutil.Equal(t, "POST", r.Method) body, err := io.ReadAll(r.Body) - require.NoError(t, err) + testutil.RequireNoError(t, err) var req CreatePageRequest err = json.Unmarshal(body, &req) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, "123456", req.SpaceID) - assert.Equal(t, "New Page", req.Title) - assert.NotNil(t, req.Body) - assert.NotNil(t, req.Body.Storage) - assert.Equal(t, "

Content

", req.Body.Storage.Value) + testutil.Equal(t, "123456", req.SpaceID) + testutil.Equal(t, "New Page", req.Title) + testutil.NotNil(t, req.Body) + testutil.NotNil(t, req.Body.Storage) + testutil.Equal(t, "

Content

", req.Body.Storage.Value) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -141,26 +140,26 @@ func TestClient_CreatePage(t *testing.T) { } page, err := client.CreatePage(context.Background(), req) - require.NoError(t, err) - assert.Equal(t, "99999", page.ID) - assert.Equal(t, "New Page", page.Title) + testutil.RequireNoError(t, err) + testutil.Equal(t, "99999", page.ID) + testutil.Equal(t, "New Page", page.Title) } func TestClient_UpdatePage(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/pages/98765", r.URL.Path) - assert.Equal(t, "PUT", r.Method) + testutil.Equal(t, "/api/v2/pages/98765", r.URL.Path) + testutil.Equal(t, "PUT", r.Method) body, err := io.ReadAll(r.Body) - require.NoError(t, err) + testutil.RequireNoError(t, err) var req UpdatePageRequest err = json.Unmarshal(body, &req) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, "98765", req.ID) - assert.Equal(t, "Updated Title", req.Title) - assert.Equal(t, 6, req.Version.Number) + testutil.Equal(t, "98765", req.ID) + testutil.Equal(t, "Updated Title", req.Title) + testutil.Equal(t, 6, req.Version.Number) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -186,15 +185,15 @@ func TestClient_UpdatePage(t *testing.T) { } page, err := client.UpdatePage(context.Background(), "98765", req) - require.NoError(t, err) - assert.Equal(t, "Updated Title", page.Title) - assert.Equal(t, 6, page.Version.Number) + testutil.RequireNoError(t, err) + testutil.Equal(t, "Updated Title", page.Title) + testutil.Equal(t, 6, page.Version.Number) } func TestClient_DeletePage(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/pages/98765", r.URL.Path) - assert.Equal(t, "DELETE", r.Method) + testutil.Equal(t, "/api/v2/pages/98765", r.URL.Path) + testutil.Equal(t, "DELETE", r.Method) w.WriteHeader(http.StatusNoContent) })) @@ -203,13 +202,13 @@ func TestClient_DeletePage(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") err := client.DeletePage(context.Background(), "98765") - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_MovePage_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/content/12345/move/append/67890", r.URL.Path) - assert.Equal(t, "PUT", r.Method) + testutil.Equal(t, "/rest/api/content/12345/move/append/67890", r.URL.Path) + testutil.Equal(t, "PUT", r.Method) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{}`)) @@ -219,7 +218,7 @@ func TestClient_MovePage_Success(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") err := client.MovePage(context.Background(), "12345", "67890") - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_MovePage_NotFound(t *testing.T) { @@ -232,7 +231,7 @@ func TestClient_MovePage_NotFound(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") err := client.MovePage(context.Background(), "99999", "67890") - require.Error(t, err) + testutil.RequireError(t, err) } func TestClient_MovePage_PermissionDenied(t *testing.T) { @@ -245,31 +244,31 @@ func TestClient_MovePage_PermissionDenied(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") err := client.MovePage(context.Background(), "12345", "67890") - require.Error(t, err) + testutil.RequireError(t, err) } func TestClient_CopyPage_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/content/12345/copy", r.URL.Path) - assert.Equal(t, "POST", r.Method) + testutil.Equal(t, "/rest/api/content/12345/copy", r.URL.Path) + testutil.Equal(t, "POST", r.Method) body, err := io.ReadAll(r.Body) - require.NoError(t, err) + testutil.RequireNoError(t, err) var req map[string]interface{} err = json.Unmarshal(body, &req) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, "New Title", req["pageTitle"]) - assert.Equal(t, true, req["copyAttachments"]) - assert.Equal(t, true, req["copyPermissions"]) - assert.Equal(t, true, req["copyProperties"]) - assert.Equal(t, true, req["copyLabels"]) - assert.Equal(t, true, req["copyCustomContents"]) + testutil.Equal(t, "New Title", req["pageTitle"]) + testutil.Equal(t, true, req["copyAttachments"]) + testutil.Equal(t, true, req["copyPermissions"]) + testutil.Equal(t, true, req["copyProperties"]) + testutil.Equal(t, true, req["copyLabels"]) + testutil.Equal(t, true, req["copyCustomContents"]) dest := req["destination"].(map[string]interface{}) - assert.Equal(t, "space", dest["type"]) - assert.Equal(t, "TEST", dest["value"]) + testutil.Equal(t, "space", dest["type"]) + testutil.Equal(t, "TEST", dest["value"]) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -296,28 +295,28 @@ func TestClient_CopyPage_Success(t *testing.T) { } page, err := client.CopyPage(context.Background(), "12345", opts) - require.NoError(t, err) - assert.Equal(t, "99999", page.ID) - assert.Equal(t, "New Title", page.Title) - assert.Equal(t, "TEST", page.SpaceID) - assert.Equal(t, 1, page.Version.Number) - assert.Equal(t, "/spaces/TEST/pages/99999", page.Links.WebUI) + testutil.RequireNoError(t, err) + testutil.Equal(t, "99999", page.ID) + testutil.Equal(t, "New Title", page.Title) + testutil.Equal(t, "TEST", page.SpaceID) + testutil.Equal(t, 1, page.Version.Number) + testutil.Equal(t, "/spaces/TEST/pages/99999", page.Links.WebUI) } func TestClient_CopyPage_MissingTitle(t *testing.T) { client := NewClient("http://unused", "user@example.com", "token") _, err := client.CopyPage(context.Background(), "12345", &CopyPageOptions{}) - require.Error(t, err) - assert.Contains(t, err.Error(), "title is required") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "title is required") } func TestClient_CopyPage_NilOptions(t *testing.T) { client := NewClient("http://unused", "user@example.com", "token") _, err := client.CopyPage(context.Background(), "12345", nil) - require.Error(t, err) - assert.Contains(t, err.Error(), "title is required") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "title is required") } func TestClient_CopyPage_APIError(t *testing.T) { @@ -334,19 +333,19 @@ func TestClient_CopyPage_APIError(t *testing.T) { } _, err := client.CopyPage(context.Background(), "99999", opts) - require.Error(t, err) + testutil.RequireError(t, err) } func TestClient_CopyPage_WithoutAttachments(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) - require.NoError(t, err) + testutil.RequireNoError(t, err) var req map[string]interface{} err = json.Unmarshal(body, &req) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, false, req["copyAttachments"]) + testutil.Equal(t, false, req["copyAttachments"]) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -367,21 +366,21 @@ func TestClient_CopyPage_WithoutAttachments(t *testing.T) { } _, err := client.CopyPage(context.Background(), "12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_CopyPage_ToDifferentSpace(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) - require.NoError(t, err) + testutil.RequireNoError(t, err) var req map[string]interface{} err = json.Unmarshal(body, &req) - require.NoError(t, err) + testutil.RequireNoError(t, err) dest := req["destination"].(map[string]interface{}) - assert.Equal(t, "space", dest["type"]) - assert.Equal(t, "OTHERSPACE", dest["value"]) + testutil.Equal(t, "space", dest["type"]) + testutil.Equal(t, "OTHERSPACE", dest["value"]) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -401,21 +400,21 @@ func TestClient_CopyPage_ToDifferentSpace(t *testing.T) { } page, err := client.CopyPage(context.Background(), "12345", opts) - require.NoError(t, err) - assert.Equal(t, "OTHERSPACE", page.SpaceID) + testutil.RequireNoError(t, err) + testutil.Equal(t, "OTHERSPACE", page.SpaceID) } func TestClient_CopyPage_WithoutLabels(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) - require.NoError(t, err) + testutil.RequireNoError(t, err) var req map[string]interface{} err = json.Unmarshal(body, &req) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, false, req["copyLabels"]) - assert.Equal(t, true, req["copyAttachments"]) // others should still be true + testutil.Equal(t, false, req["copyLabels"]) + testutil.Equal(t, true, req["copyAttachments"]) // others should still be true w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -437,7 +436,7 @@ func TestClient_CopyPage_WithoutLabels(t *testing.T) { } _, err := client.CopyPage(context.Background(), "12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_UpdatePage_VersionConflict(t *testing.T) { @@ -459,8 +458,8 @@ func TestClient_UpdatePage_VersionConflict(t *testing.T) { } _, err := client.UpdatePage(context.Background(), "98765", req) - require.Error(t, err) - assert.Contains(t, err.Error(), "conflict") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "conflict") } func TestClient_GetPage_MissingBody(t *testing.T) { @@ -478,10 +477,10 @@ func TestClient_GetPage_MissingBody(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") page, err := client.GetPage(context.Background(), "98765", nil) - require.NoError(t, err) - assert.Equal(t, "98765", page.ID) - assert.Equal(t, "Page Without Body", page.Title) - assert.Nil(t, page.Body) + testutil.RequireNoError(t, err) + testutil.Equal(t, "98765", page.ID) + testutil.Equal(t, "Page Without Body", page.Title) + testutil.Nil(t, page.Body) } func TestClient_GetPage_EmptyBodyStorage(t *testing.T) { @@ -502,9 +501,9 @@ func TestClient_GetPage_EmptyBodyStorage(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") page, err := client.GetPage(context.Background(), "98765", nil) - require.NoError(t, err) - assert.NotNil(t, page.Body) - assert.Nil(t, page.Body.Storage) + testutil.RequireNoError(t, err) + testutil.NotNil(t, page.Body) + testutil.Nil(t, page.Body.Storage) } func TestClient_ListPages_WithCursor(t *testing.T) { @@ -514,7 +513,7 @@ func TestClient_ListPages_WithCursor(t *testing.T) { if callCount == 1 { // First call - return results with cursor - assert.Empty(t, r.URL.Query().Get("cursor")) + testutil.Empty(t, r.URL.Query().Get("cursor")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ "results": [{"id": "1", "title": "Page 1"}], @@ -522,7 +521,7 @@ func TestClient_ListPages_WithCursor(t *testing.T) { }`)) } else { // Second call - verify cursor is passed - assert.Equal(t, "abc123", r.URL.Query().Get("cursor")) + testutil.Equal(t, "abc123", r.URL.Query().Get("cursor")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ "results": [{"id": "2", "title": "Page 2"}], @@ -536,16 +535,16 @@ func TestClient_ListPages_WithCursor(t *testing.T) { // First request result1, err := client.ListPages(context.Background(), "123", nil) - require.NoError(t, err) - assert.True(t, result1.HasMore()) + testutil.RequireNoError(t, err) + testutil.True(t, result1.HasMore()) // Second request with cursor opts := &ListPagesOptions{Cursor: "abc123"} result2, err := client.ListPages(context.Background(), "123", opts) - require.NoError(t, err) - assert.False(t, result2.HasMore()) + testutil.RequireNoError(t, err) + testutil.False(t, result2.HasMore()) - assert.Equal(t, 2, callCount) + testutil.Equal(t, 2, callCount) } func TestClient_ListPages_EmptyResults(t *testing.T) { @@ -558,9 +557,9 @@ func TestClient_ListPages_EmptyResults(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") result, err := client.ListPages(context.Background(), "123", nil) - require.NoError(t, err) - assert.Empty(t, result.Results) - assert.False(t, result.HasMore()) + testutil.RequireNoError(t, err) + testutil.Empty(t, result.Results) + testutil.False(t, result.HasMore()) } func TestClient_ListPages_NullVersion(t *testing.T) { @@ -580,7 +579,7 @@ func TestClient_ListPages_NullVersion(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") result, err := client.ListPages(context.Background(), "123", nil) - require.NoError(t, err) - require.Len(t, result.Results, 1) - assert.Nil(t, result.Results[0].Version) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Results, 1) + testutil.Nil(t, result.Results[0].Version) } diff --git a/tools/cfl/api/search_test.go b/tools/cfl/api/search_test.go index 650c924..0272914 100644 --- a/tools/cfl/api/search_test.go +++ b/tools/cfl/api/search_test.go @@ -6,18 +6,17 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestClient_Search_Success(t *testing.T) { testData := loadTestData(t, "search.json") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/search", r.URL.Path) - assert.Equal(t, "GET", r.Method) - assert.Contains(t, r.URL.Query().Get("cql"), `text ~ "test"`) - assert.Equal(t, "highlight", r.URL.Query().Get("excerpt")) + testutil.Equal(t, "/rest/api/search", r.URL.Path) + testutil.Equal(t, "GET", r.Method) + testutil.Contains(t, r.URL.Query().Get("cql"), `text ~ "test"`) + testutil.Equal(t, "highlight", r.URL.Query().Get("excerpt")) w.WriteHeader(http.StatusOK) _, _ = w.Write(testData) @@ -29,17 +28,17 @@ func TestClient_Search_Success(t *testing.T) { Text: "test", }) - require.NoError(t, err) - assert.Len(t, result.Results, 2) - assert.Equal(t, 50, result.TotalSize) - assert.True(t, result.HasMore()) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Results, 2) + testutil.Equal(t, 50, result.TotalSize) + testutil.True(t, result.HasMore()) // Check first result first := result.Results[0] - assert.Equal(t, "12345", first.Content.ID) - assert.Equal(t, "page", first.Content.Type) - assert.Equal(t, "Getting Started Guide", first.Content.Title) - assert.Equal(t, "Development", first.ResultGlobalContainer.Title) + testutil.Equal(t, "12345", first.Content.ID) + testutil.Equal(t, "page", first.Content.Type) + testutil.Equal(t, "Getting Started Guide", first.Content.Title) + testutil.Equal(t, "Development", first.ResultGlobalContainer.Title) } func TestClient_Search_EmptyResults(t *testing.T) { @@ -61,20 +60,20 @@ func TestClient_Search_EmptyResults(t *testing.T) { Text: "nonexistent", }) - require.NoError(t, err) - assert.Len(t, result.Results, 0) - assert.False(t, result.HasMore()) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Results, 0) + testutil.False(t, result.HasMore()) } func TestClient_Search_WithAllOptions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cql := r.URL.Query().Get("cql") - assert.Contains(t, cql, `text ~ "search term"`) - assert.Contains(t, cql, `space = "DEV"`) - assert.Contains(t, cql, `type = "page"`) - assert.Contains(t, cql, `title ~ "guide"`) - assert.Contains(t, cql, `label = "documentation"`) - assert.Equal(t, "50", r.URL.Query().Get("limit")) + testutil.Contains(t, cql, `text ~ "search term"`) + testutil.Contains(t, cql, `space = "DEV"`) + testutil.Contains(t, cql, `type = "page"`) + testutil.Contains(t, cql, `title ~ "guide"`) + testutil.Contains(t, cql, `label = "documentation"`) + testutil.Equal(t, "50", r.URL.Query().Get("limit")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": [], "totalSize": 0}`)) @@ -90,14 +89,14 @@ func TestClient_Search_WithAllOptions(t *testing.T) { Label: "documentation", Limit: 50, }) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_Search_RawCQL(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Raw CQL should be used as-is cql := r.URL.Query().Get("cql") - assert.Equal(t, `type=page AND lastModified > now("-7d")`, cql) + testutil.Equal(t, `type=page AND lastModified > now("-7d")`, cql) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": [], "totalSize": 0}`)) @@ -109,23 +108,23 @@ func TestClient_Search_RawCQL(t *testing.T) { CQL: `type=page AND lastModified > now("-7d")`, Text: "ignored", // Should be ignored when CQL is set }) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_Search_NoQuery(t *testing.T) { client := NewClient("http://unused", "user@example.com", "token") _, err := client.Search(context.Background(), &SearchOptions{}) - require.Error(t, err) - assert.Contains(t, err.Error(), "search requires a query or filters") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "search requires a query or filters") } func TestClient_Search_NilOptions(t *testing.T) { client := NewClient("http://unused", "user@example.com", "token") _, err := client.Search(context.Background(), nil) - require.Error(t, err) - assert.Contains(t, err.Error(), "search requires a query or filters") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "search requires a query or filters") } func TestClient_Search_APIError(t *testing.T) { @@ -166,8 +165,8 @@ func TestClient_Search_APIError(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") _, err := client.Search(context.Background(), &SearchOptions{Text: "test"}) - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errContain) + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), tt.errContain) }) } } @@ -182,8 +181,8 @@ func TestClient_Search_MalformedResponse(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") _, err := client.Search(context.Background(), &SearchOptions{Text: "test"}) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to parse search response") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to parse search response") } func TestClient_Search_Pagination(t *testing.T) { @@ -207,7 +206,7 @@ func TestClient_Search_Pagination(t *testing.T) { Size: tt.size, TotalSize: tt.total, } - assert.Equal(t, tt.expected, resp.HasMore()) + testutil.Equal(t, tt.expected, resp.HasMore()) }) } } @@ -215,31 +214,31 @@ func TestClient_Search_Pagination(t *testing.T) { func TestBuildCQL_TextOnly(t *testing.T) { opts := &SearchOptions{Text: "hello world"} cql := buildCQL(opts) - assert.Equal(t, `text ~ "hello world"`, cql) + testutil.Equal(t, `text ~ "hello world"`, cql) } func TestBuildCQL_SpaceFilter(t *testing.T) { opts := &SearchOptions{Space: "DEV"} cql := buildCQL(opts) - assert.Equal(t, `space = "DEV"`, cql) + testutil.Equal(t, `space = "DEV"`, cql) } func TestBuildCQL_TypeFilter(t *testing.T) { opts := &SearchOptions{Type: "page"} cql := buildCQL(opts) - assert.Equal(t, `type = "page"`, cql) + testutil.Equal(t, `type = "page"`, cql) } func TestBuildCQL_TitleFilter(t *testing.T) { opts := &SearchOptions{Title: "Getting Started"} cql := buildCQL(opts) - assert.Equal(t, `title ~ "Getting Started"`, cql) + testutil.Equal(t, `title ~ "Getting Started"`, cql) } func TestBuildCQL_LabelFilter(t *testing.T) { opts := &SearchOptions{Label: "documentation"} cql := buildCQL(opts) - assert.Equal(t, `label = "documentation"`, cql) + testutil.Equal(t, `label = "documentation"`, cql) } func TestBuildCQL_Combined(t *testing.T) { @@ -249,21 +248,21 @@ func TestBuildCQL_Combined(t *testing.T) { Type: "page", } cql := buildCQL(opts) - assert.Contains(t, cql, `text ~ "api"`) - assert.Contains(t, cql, `space = "DEV"`) - assert.Contains(t, cql, `type = "page"`) - assert.Contains(t, cql, " AND ") + testutil.Contains(t, cql, `text ~ "api"`) + testutil.Contains(t, cql, `space = "DEV"`) + testutil.Contains(t, cql, `type = "page"`) + testutil.Contains(t, cql, " AND ") } func TestBuildCQL_Empty(t *testing.T) { opts := &SearchOptions{} cql := buildCQL(opts) - assert.Empty(t, cql) + testutil.Empty(t, cql) } func TestBuildCQL_QuotesInValue(t *testing.T) { opts := &SearchOptions{Text: `search "quoted" term`} cql := buildCQL(opts) // Go's %q escapes quotes properly - assert.Contains(t, cql, `text ~ "search \"quoted\" term"`) + testutil.Contains(t, cql, `text ~ "search \"quoted\" term"`) } diff --git a/tools/cfl/api/space_management_test.go b/tools/cfl/api/space_management_test.go index b5951ba..b014dec 100644 --- a/tools/cfl/api/space_management_test.go +++ b/tools/cfl/api/space_management_test.go @@ -7,21 +7,20 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestClient_CreateSpace(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "POST", r.Method) - assert.Equal(t, "/api/v2/spaces", r.URL.Path) + testutil.Equal(t, "POST", r.Method) + testutil.Equal(t, "/api/v2/spaces", r.URL.Path) var req CreateSpaceRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) - assert.Equal(t, "TEST", req.Key) - assert.Equal(t, "Test Space", req.Name) - assert.Equal(t, "global", req.Type) + testutil.RequireNoError(t, err) + testutil.Equal(t, "TEST", req.Key) + testutil.Equal(t, "Test Space", req.Name) + testutil.Equal(t, "global", req.Type) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -42,21 +41,21 @@ func TestClient_CreateSpace(t *testing.T) { Type: "global", }) - require.NoError(t, err) - assert.Equal(t, "123456", space.ID) - assert.Equal(t, "TEST", space.Key) - assert.Equal(t, "Test Space", space.Name) - assert.Equal(t, "global", space.Type) - assert.Equal(t, "/spaces/TEST", space.Links.WebUI) + testutil.RequireNoError(t, err) + testutil.Equal(t, "123456", space.ID) + testutil.Equal(t, "TEST", space.Key) + testutil.Equal(t, "Test Space", space.Name) + testutil.Equal(t, "global", space.Type) + testutil.Equal(t, "/spaces/TEST", space.Links.WebUI) } func TestClient_CreateSpace_WithDescription(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req CreateSpaceRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) - assert.NotNil(t, req.Description) - assert.Equal(t, "A test space", req.Description.Plain.Value) + testutil.RequireNoError(t, err) + testutil.NotNil(t, req.Description) + testutil.Equal(t, "A test space", req.Description.Plain.Value) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -79,10 +78,10 @@ func TestClient_CreateSpace_WithDescription(t *testing.T) { }, }) - require.NoError(t, err) - assert.Equal(t, "TEST", space.Key) - assert.NotNil(t, space.Description) - assert.Equal(t, "A test space", space.Description.Plain.Value) + testutil.RequireNoError(t, err) + testutil.Equal(t, "TEST", space.Key) + testutil.NotNil(t, space.Description) + testutil.Equal(t, "A test space", space.Description.Plain.Value) } func TestClient_CreateSpace_Error(t *testing.T) { @@ -98,19 +97,19 @@ func TestClient_CreateSpace_Error(t *testing.T) { Name: "Duplicate", }) - require.Error(t, err) + testutil.RequireError(t, err) } func TestClient_UpdateSpace(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "PUT", r.Method) - assert.Equal(t, "/rest/api/space/TEST", r.URL.Path) + testutil.Equal(t, "PUT", r.Method) + testutil.Equal(t, "/rest/api/space/TEST", r.URL.Path) var req UpdateSpaceRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) - assert.Equal(t, "TEST", req.Key) - assert.Equal(t, "Updated Name", req.Name) + testutil.RequireNoError(t, err) + testutil.Equal(t, "TEST", req.Key) + testutil.Equal(t, "Updated Name", req.Name) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -130,23 +129,23 @@ func TestClient_UpdateSpace(t *testing.T) { Name: "Updated Name", }) - require.NoError(t, err) - assert.Equal(t, "123456", space.ID) - assert.Equal(t, "TEST", space.Key) - assert.Equal(t, "Updated Name", space.Name) - assert.Equal(t, "global", space.Type) - assert.NotNil(t, space.Description) - assert.Equal(t, "Description", space.Description.Plain.Value) + testutil.RequireNoError(t, err) + testutil.Equal(t, "123456", space.ID) + testutil.Equal(t, "TEST", space.Key) + testutil.Equal(t, "Updated Name", space.Name) + testutil.Equal(t, "global", space.Type) + testutil.NotNil(t, space.Description) + testutil.Equal(t, "Description", space.Description.Plain.Value) } func TestClient_UpdateSpace_WithDescription(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req UpdateSpaceRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) - assert.NotNil(t, req.Description) - assert.Equal(t, "New description", req.Description.Plain.Value) - assert.Equal(t, "plain", req.Description.Plain.Representation) + testutil.RequireNoError(t, err) + testutil.NotNil(t, req.Description) + testutil.Equal(t, "New description", req.Description.Plain.Value) + testutil.Equal(t, "plain", req.Description.Plain.Representation) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -172,8 +171,8 @@ func TestClient_UpdateSpace_WithDescription(t *testing.T) { }, }) - require.NoError(t, err) - assert.Equal(t, "New description", space.Description.Plain.Value) + testutil.RequireNoError(t, err) + testutil.Equal(t, "New description", space.Description.Plain.Value) } func TestClient_UpdateSpace_NotFound(t *testing.T) { @@ -189,7 +188,7 @@ func TestClient_UpdateSpace_NotFound(t *testing.T) { Name: "Updated", }) - require.Error(t, err) + testutil.RequireError(t, err) } func TestClient_UpdateSpace_NoDescription(t *testing.T) { @@ -212,14 +211,14 @@ func TestClient_UpdateSpace_NoDescription(t *testing.T) { Name: "Test Space", }) - require.NoError(t, err) - assert.Nil(t, space.Description) + testutil.RequireNoError(t, err) + testutil.Nil(t, space.Description) } func TestClient_DeleteSpace(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "DELETE", r.Method) - assert.Equal(t, "/rest/api/space/TEST", r.URL.Path) + testutil.Equal(t, "DELETE", r.Method) + testutil.Equal(t, "/rest/api/space/TEST", r.URL.Path) w.WriteHeader(http.StatusAccepted) })) @@ -228,7 +227,7 @@ func TestClient_DeleteSpace(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") err := client.DeleteSpace(context.Background(), "TEST") - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_DeleteSpace_NotFound(t *testing.T) { @@ -241,7 +240,7 @@ func TestClient_DeleteSpace_NotFound(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") err := client.DeleteSpace(context.Background(), "NOPE") - require.Error(t, err) + testutil.RequireError(t, err) } func TestV1SpaceResponse_ToSpace(t *testing.T) { @@ -257,13 +256,13 @@ func TestV1SpaceResponse_ToSpace(t *testing.T) { space := response.toSpace() - assert.Equal(t, "123456", space.ID) - assert.Equal(t, "TEST", space.Key) - assert.Equal(t, "Test Space", space.Name) - assert.Equal(t, "global", space.Type) - assert.Equal(t, "/spaces/TEST", space.Links.WebUI) - assert.NotNil(t, space.Description) - assert.Equal(t, "A test space", space.Description.Plain.Value) + testutil.Equal(t, "123456", space.ID) + testutil.Equal(t, "TEST", space.Key) + testutil.Equal(t, "Test Space", space.Name) + testutil.Equal(t, "global", space.Type) + testutil.Equal(t, "/spaces/TEST", space.Links.WebUI) + testutil.NotNil(t, space.Description) + testutil.Equal(t, "A test space", space.Description.Plain.Value) } func TestV1SpaceResponse_ToSpace_EmptyDescription(t *testing.T) { @@ -276,5 +275,5 @@ func TestV1SpaceResponse_ToSpace_EmptyDescription(t *testing.T) { space := response.toSpace() - assert.Nil(t, space.Description) + testutil.Nil(t, space.Description) } diff --git a/tools/cfl/api/spaces_test.go b/tools/cfl/api/spaces_test.go index 9decb7c..08c6604 100644 --- a/tools/cfl/api/spaces_test.go +++ b/tools/cfl/api/spaces_test.go @@ -6,16 +6,16 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func loadTestData(t *testing.T, filename string) []byte { t.Helper() data, err := os.ReadFile(filepath.Join("testdata", filename)) - require.NoError(t, err) + testutil.RequireNoError(t, err) return data } @@ -23,11 +23,11 @@ func TestClient_ListSpaces(t *testing.T) { testData := loadTestData(t, "spaces.json") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/spaces", r.URL.Path) - assert.Equal(t, "GET", r.Method) + testutil.Equal(t, "/api/v2/spaces", r.URL.Path) + testutil.Equal(t, "GET", r.Method) // Check query params - assert.Equal(t, "25", r.URL.Query().Get("limit")) + testutil.Equal(t, "25", r.URL.Query().Get("limit")) w.WriteHeader(http.StatusOK) _, _ = w.Write(testData) @@ -37,23 +37,23 @@ func TestClient_ListSpaces(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") result, err := client.ListSpaces(context.Background(), nil) - require.NoError(t, err) - assert.Len(t, result.Results, 2) - assert.True(t, result.HasMore()) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Results, 2) + testutil.True(t, result.HasMore()) // Check first space space := result.Results[0] - assert.Equal(t, "123456", space.ID) - assert.Equal(t, "DEV", space.Key) - assert.Equal(t, "Development", space.Name) - assert.Equal(t, "global", space.Type) + testutil.Equal(t, "123456", space.ID) + testutil.Equal(t, "DEV", space.Key) + testutil.Equal(t, "Development", space.Name) + testutil.Equal(t, "global", space.Type) } func TestClient_ListSpaces_WithOptions(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "50", r.URL.Query().Get("limit")) - assert.Equal(t, "global", r.URL.Query().Get("type")) - assert.Equal(t, "current", r.URL.Query().Get("status")) + testutil.Equal(t, "50", r.URL.Query().Get("limit")) + testutil.Equal(t, "global", r.URL.Query().Get("type")) + testutil.Equal(t, "current", r.URL.Query().Get("status")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": []}`)) @@ -67,13 +67,13 @@ func TestClient_ListSpaces_WithOptions(t *testing.T) { Status: "current", } _, err := client.ListSpaces(context.Background(), opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestClient_GetSpace(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/spaces/123456", r.URL.Path) - assert.Equal(t, "GET", r.Method) + testutil.Equal(t, "/api/v2/spaces/123456", r.URL.Path) + testutil.Equal(t, "GET", r.Method) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -88,10 +88,10 @@ func TestClient_GetSpace(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") space, err := client.GetSpace(context.Background(), "123456") - require.NoError(t, err) - assert.Equal(t, "123456", space.ID) - assert.Equal(t, "DEV", space.Key) - assert.Equal(t, "Development", space.Name) + testutil.RequireNoError(t, err) + testutil.Equal(t, "123456", space.ID) + testutil.Equal(t, "DEV", space.Key) + testutil.Equal(t, "Development", space.Name) } func TestClient_GetSpace_NotFound(t *testing.T) { @@ -104,15 +104,15 @@ func TestClient_GetSpace_NotFound(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") _, err := client.GetSpace(context.Background(), "invalid") - require.Error(t, err) - assert.Contains(t, err.Error(), "Space not found") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "Space not found") } func TestClient_GetSpaceByKey_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/spaces", r.URL.Path) - assert.Equal(t, "DEV", r.URL.Query().Get("keys")) - assert.Equal(t, "1", r.URL.Query().Get("limit")) + testutil.Equal(t, "/api/v2/spaces", r.URL.Path) + testutil.Equal(t, "DEV", r.URL.Query().Get("keys")) + testutil.Equal(t, "1", r.URL.Query().Get("limit")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -129,15 +129,15 @@ func TestClient_GetSpaceByKey_Success(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") space, err := client.GetSpaceByKey(context.Background(), "DEV") - require.NoError(t, err) - assert.Equal(t, "123456", space.ID) - assert.Equal(t, "DEV", space.Key) - assert.Equal(t, "Development", space.Name) + testutil.RequireNoError(t, err) + testutil.Equal(t, "123456", space.ID) + testutil.Equal(t, "DEV", space.Key) + testutil.Equal(t, "Development", space.Name) } func TestClient_GetSpaceByKey_NotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "NONEXISTENT", r.URL.Query().Get("keys")) + testutil.Equal(t, "NONEXISTENT", r.URL.Query().Get("keys")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": []}`)) @@ -147,17 +147,18 @@ func TestClient_GetSpaceByKey_NotFound(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") _, err := client.GetSpaceByKey(context.Background(), "NONEXISTENT") - require.Error(t, err) - assert.Contains(t, err.Error(), "not found") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "not found") } func TestClient_ListSpaces_WithMultipleKeys(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check that multiple keys are passed correctly keys := r.URL.Query()["keys"] - assert.Contains(t, keys, "DEV") - assert.Contains(t, keys, "PROD") - assert.Contains(t, keys, "TEST") + keysStr := strings.Join(keys, ",") + testutil.Contains(t, keysStr, "DEV") + testutil.Contains(t, keysStr, "PROD") + testutil.Contains(t, keysStr, "TEST") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -176,8 +177,8 @@ func TestClient_ListSpaces_WithMultipleKeys(t *testing.T) { } result, err := client.ListSpaces(context.Background(), opts) - require.NoError(t, err) - assert.Len(t, result.Results, 3) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Results, 3) } func TestClient_ListSpaces_EmptyResults(t *testing.T) { @@ -190,9 +191,9 @@ func TestClient_ListSpaces_EmptyResults(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") result, err := client.ListSpaces(context.Background(), nil) - require.NoError(t, err) - assert.Empty(t, result.Results) - assert.False(t, result.HasMore()) + testutil.RequireNoError(t, err) + testutil.Empty(t, result.Results) + testutil.False(t, result.HasMore()) } func TestClient_ListSpaces_NullDescription(t *testing.T) { @@ -212,9 +213,9 @@ func TestClient_ListSpaces_NullDescription(t *testing.T) { client := NewClient(server.URL, "user@example.com", "token") result, err := client.ListSpaces(context.Background(), nil) - require.NoError(t, err) - require.Len(t, result.Results, 1) - assert.Equal(t, "TEST", result.Results[0].Key) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Results, 1) + testutil.Equal(t, "TEST", result.Results[0].Key) } func TestClient_ListSpaces_APIError(t *testing.T) { @@ -227,5 +228,5 @@ func TestClient_ListSpaces_APIError(t *testing.T) { client := NewClient(server.URL, "user@example.com", "bad-token") _, err := client.ListSpaces(context.Background(), nil) - require.Error(t, err) + testutil.RequireError(t, err) } diff --git a/tools/cfl/go.mod b/tools/cfl/go.mod index 3e7bd90..517e536 100644 --- a/tools/cfl/go.mod +++ b/tools/cfl/go.mod @@ -7,7 +7,6 @@ require ( github.com/charmbracelet/huh v0.8.0 github.com/open-cli-collective/atlassian-go v0.0.0-00010101000000-000000000000 github.com/spf13/cobra v1.8.1 - github.com/stretchr/testify v1.11.1 github.com/yuin/goldmark v1.7.16 gopkg.in/yaml.v3 v3.0.1 ) @@ -29,7 +28,6 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.18.0 // indirect diff --git a/tools/cfl/go.sum b/tools/cfl/go.sum index 4c61c5a..7396594 100644 --- a/tools/cfl/go.sum +++ b/tools/cfl/go.sum @@ -47,8 +47,6 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -89,8 +87,6 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= diff --git a/tools/cfl/internal/cmd/attachment/delete_test.go b/tools/cfl/internal/cmd/attachment/delete_test.go index b1effc9..7a302b1 100644 --- a/tools/cfl/internal/cmd/attachment/delete_test.go +++ b/tools/cfl/internal/cmd/attachment/delete_test.go @@ -7,8 +7,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -52,8 +51,8 @@ func newTestRootOptions() *root.Options { func TestRunDeleteAttachment_ForceDelete(t *testing.T) { server := mockAttachmentServer(t, nil, func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "DELETE", r.Method) - assert.Equal(t, "/api/v2/attachments/att123", r.URL.Path) + testutil.Equal(t, "DELETE", r.Method) + testutil.Equal(t, "/api/v2/attachments/att123", r.URL.Path) w.WriteHeader(http.StatusNoContent) }) defer server.Close() @@ -68,7 +67,7 @@ func TestRunDeleteAttachment_ForceDelete(t *testing.T) { } err := runDeleteAttachment("att123", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunDeleteAttachment_ConfirmWithY(t *testing.T) { @@ -90,8 +89,8 @@ func TestRunDeleteAttachment_ConfirmWithY(t *testing.T) { } err := runDeleteAttachment("att123", opts) - require.NoError(t, err) - assert.True(t, deleted, "attachment should have been deleted") + testutil.RequireNoError(t, err) + testutil.True(t, deleted, "attachment should have been deleted") } func TestRunDeleteAttachment_ConfirmWithUpperY(t *testing.T) { @@ -113,8 +112,8 @@ func TestRunDeleteAttachment_ConfirmWithUpperY(t *testing.T) { } err := runDeleteAttachment("att123", opts) - require.NoError(t, err) - assert.True(t, deleted, "attachment should have been deleted") + testutil.RequireNoError(t, err) + testutil.True(t, deleted, "attachment should have been deleted") } func TestRunDeleteAttachment_CancelWithN(t *testing.T) { @@ -136,8 +135,8 @@ func TestRunDeleteAttachment_CancelWithN(t *testing.T) { } err := runDeleteAttachment("att123", opts) - require.NoError(t, err) - assert.False(t, deleted, "attachment should NOT have been deleted") + testutil.RequireNoError(t, err) + testutil.False(t, deleted, "attachment should NOT have been deleted") } func TestRunDeleteAttachment_CancelWithEmpty(t *testing.T) { @@ -159,8 +158,8 @@ func TestRunDeleteAttachment_CancelWithEmpty(t *testing.T) { } err := runDeleteAttachment("att123", opts) - require.NoError(t, err) - assert.False(t, deleted, "attachment should NOT have been deleted") + testutil.RequireNoError(t, err) + testutil.False(t, deleted, "attachment should NOT have been deleted") } func TestRunDeleteAttachment_CancelWithOther(t *testing.T) { @@ -182,8 +181,8 @@ func TestRunDeleteAttachment_CancelWithOther(t *testing.T) { } err := runDeleteAttachment("att123", opts) - require.NoError(t, err) - assert.False(t, deleted, "attachment should NOT have been deleted") + testutil.RequireNoError(t, err) + testutil.False(t, deleted, "attachment should NOT have been deleted") } func TestRunDeleteAttachment_GetAttachmentFails(t *testing.T) { @@ -206,8 +205,8 @@ func TestRunDeleteAttachment_GetAttachmentFails(t *testing.T) { } err := runDeleteAttachment("invalid", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get attachment") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to get attachment") } func TestRunDeleteAttachment_DeleteFails(t *testing.T) { @@ -229,6 +228,6 @@ func TestRunDeleteAttachment_DeleteFails(t *testing.T) { } err := runDeleteAttachment("att123", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to delete attachment") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to delete attachment") } diff --git a/tools/cfl/internal/cmd/attachment/download_test.go b/tools/cfl/internal/cmd/attachment/download_test.go index aff0d52..2d0f2f5 100644 --- a/tools/cfl/internal/cmd/attachment/download_test.go +++ b/tools/cfl/internal/cmd/attachment/download_test.go @@ -8,8 +8,7 @@ import ( "path/filepath" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -149,12 +148,12 @@ func TestRunDownload_Success(t *testing.T) { } err := runDownload("att123", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify file was created content, err := os.ReadFile(filepath.Join(tmpDir, "document.pdf")) - require.NoError(t, err) - assert.Equal(t, "fake pdf content", string(content)) + testutil.RequireNoError(t, err) + testutil.Equal(t, "fake pdf content", string(content)) } func TestRunDownload_CustomOutputFile(t *testing.T) { @@ -174,12 +173,12 @@ func TestRunDownload_CustomOutputFile(t *testing.T) { } err := runDownload("att123", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify file was created with custom name content, err := os.ReadFile(outputPath) - require.NoError(t, err) - assert.Equal(t, "fake pdf content", string(content)) + testutil.RequireNoError(t, err) + testutil.Equal(t, "fake pdf content", string(content)) } func TestRunDownload_FileExists_NoForce(t *testing.T) { @@ -194,7 +193,7 @@ func TestRunDownload_FileExists_NoForce(t *testing.T) { // Create existing file existingFile := filepath.Join(tmpDir, "document.pdf") err := os.WriteFile(existingFile, []byte("existing content"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) rootOpts := newDownloadTestRootOptions() client := api.NewClient(server.URL, "test@example.com", "token") @@ -206,13 +205,13 @@ func TestRunDownload_FileExists_NoForce(t *testing.T) { } err = runDownload("att123", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "file already exists") - assert.Contains(t, err.Error(), "--force") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "file already exists") + testutil.Contains(t, err.Error(), "--force") // Verify original file was not overwritten content, _ := os.ReadFile(existingFile) - assert.Equal(t, "existing content", string(content)) + testutil.Equal(t, "existing content", string(content)) } func TestRunDownload_FileExists_WithForce(t *testing.T) { @@ -227,7 +226,7 @@ func TestRunDownload_FileExists_WithForce(t *testing.T) { // Create existing file existingFile := filepath.Join(tmpDir, "document.pdf") err := os.WriteFile(existingFile, []byte("existing content"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) rootOpts := newDownloadTestRootOptions() client := api.NewClient(server.URL, "test@example.com", "token") @@ -239,11 +238,11 @@ func TestRunDownload_FileExists_WithForce(t *testing.T) { } err = runDownload("att123", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify file was overwritten content, _ := os.ReadFile(existingFile) - assert.Equal(t, "fake pdf content", string(content)) + testutil.Equal(t, "fake pdf content", string(content)) } func TestRunDownload_AttachmentNotFound(t *testing.T) { @@ -262,8 +261,8 @@ func TestRunDownload_AttachmentNotFound(t *testing.T) { } err := runDownload("nonexistent", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get attachment info") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to get attachment info") } func TestRunDownload_DownloadFailed(t *testing.T) { @@ -296,8 +295,8 @@ func TestRunDownload_DownloadFailed(t *testing.T) { } err := runDownload("att123", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to download attachment") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to download attachment") } func TestRunDownload_InvalidFilename(t *testing.T) { @@ -331,8 +330,8 @@ func TestRunDownload_InvalidFilename(t *testing.T) { } err := runDownload("att123", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid attachment filename") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid attachment filename") }) } } @@ -370,13 +369,13 @@ func TestRunDownload_PathTraversalPrevented(t *testing.T) { } err := runDownload("att123", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // File should be saved as just "passwd" (the base name), not a path traversal _, err = os.Stat(filepath.Join(tmpDir, "passwd")) - assert.NoError(t, err, "file should be saved as 'passwd' in current directory") + testutil.NoError(t, err) // Should NOT have created file outside tmpDir _, err = os.Stat("/etc/passwd-test") - assert.True(t, os.IsNotExist(err) || err != nil, "should not write outside current directory") + testutil.True(t, os.IsNotExist(err) || err != nil, "should not write outside current directory") } diff --git a/tools/cfl/internal/cmd/attachment/list_test.go b/tools/cfl/internal/cmd/attachment/list_test.go index 13563ed..3480ad5 100644 --- a/tools/cfl/internal/cmd/attachment/list_test.go +++ b/tools/cfl/internal/cmd/attachment/list_test.go @@ -6,8 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -45,7 +44,7 @@ func TestRunList_Success(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_Empty(t *testing.T) { @@ -66,7 +65,7 @@ func TestRunList_Empty(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_APIError(t *testing.T) { @@ -87,8 +86,8 @@ func TestRunList_APIError(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to list attachments") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to list attachments") } func TestRunList_JSONOutput(t *testing.T) { @@ -114,7 +113,7 @@ func TestRunList_JSONOutput(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_InvalidOutputFormat(t *testing.T) { @@ -128,8 +127,8 @@ func TestRunList_InvalidOutputFormat(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid output format") } func TestFormatFileSize(t *testing.T) { @@ -150,7 +149,7 @@ func TestFormatFileSize(t *testing.T) { for _, tt := range tests { t.Run(tt.expected, func(t *testing.T) { result := formatFileSize(tt.bytes) - assert.Equal(t, tt.expected, result) + testutil.Equal(t, tt.expected, result) }) } } @@ -203,7 +202,7 @@ func TestIsAttachmentReferenced(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isAttachmentReferenced(tt.filename, tt.content) - assert.Equal(t, tt.expected, result) + testutil.Equal(t, tt.expected, result) }) } } @@ -222,9 +221,9 @@ func TestFilterUnusedAttachments(t *testing.T) { unused := filterUnusedAttachments(attachments, content) - require.Len(t, unused, 1) - assert.Equal(t, "att2", unused[0].ID) - assert.Equal(t, "unused-doc.pdf", unused[0].Title) + testutil.Len(t, unused, 1) + testutil.Equal(t, "att2", unused[0].ID) + testutil.Equal(t, "unused-doc.pdf", unused[0].Title) } func TestFilterUnusedAttachments_AllUnused(t *testing.T) { @@ -237,7 +236,7 @@ func TestFilterUnusedAttachments_AllUnused(t *testing.T) { unused := filterUnusedAttachments(attachments, content) - require.Len(t, unused, 2) + testutil.Len(t, unused, 2) } func TestFilterUnusedAttachments_NoneUnused(t *testing.T) { @@ -249,7 +248,7 @@ func TestFilterUnusedAttachments_NoneUnused(t *testing.T) { unused := filterUnusedAttachments(attachments, content) - assert.Empty(t, unused) + testutil.Empty(t, unused) } func TestRunList_UnusedFlag(t *testing.T) { @@ -295,8 +294,8 @@ func TestRunList_UnusedFlag(t *testing.T) { } err := runList(opts) - require.NoError(t, err) - assert.Equal(t, 2, requestCount) // Both attachments and page content fetched + testutil.RequireNoError(t, err) + testutil.Equal(t, 2, requestCount) // Both attachments and page content fetched } func TestRunList_UnusedFlag_NoUnused(t *testing.T) { @@ -337,5 +336,5 @@ func TestRunList_UnusedFlag_NoUnused(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } diff --git a/tools/cfl/internal/cmd/attachment/upload_test.go b/tools/cfl/internal/cmd/attachment/upload_test.go index aa2c086..4afd9f7 100644 --- a/tools/cfl/internal/cmd/attachment/upload_test.go +++ b/tools/cfl/internal/cmd/attachment/upload_test.go @@ -8,8 +8,7 @@ import ( "path/filepath" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -29,11 +28,11 @@ func TestRunUpload_Success(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "upload.txt") err := os.WriteFile(testFile, []byte("test content"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "POST", r.Method) - assert.Contains(t, r.URL.Path, "/child/attachment") + testutil.Equal(t, "POST", r.Method) + testutil.Contains(t, r.URL.Path, "/child/attachment") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -58,19 +57,19 @@ func TestRunUpload_Success(t *testing.T) { } err = runUpload(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunUpload_WithComment(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "upload.txt") err := os.WriteFile(testFile, []byte("test content"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedComment string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(10 << 20) - require.NoError(t, err) + testutil.RequireNoError(t, err) receivedComment = r.FormValue("comment") w.WriteHeader(http.StatusOK) @@ -97,8 +96,8 @@ func TestRunUpload_WithComment(t *testing.T) { } err = runUpload(opts) - require.NoError(t, err) - assert.Equal(t, "My upload comment", receivedComment) + testutil.RequireNoError(t, err) + testutil.Equal(t, "My upload comment", receivedComment) } func TestRunUpload_FileNotFound(t *testing.T) { @@ -113,15 +112,15 @@ func TestRunUpload_FileNotFound(t *testing.T) { } err := runUpload(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to open file") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to open file") } func TestRunUpload_APIError(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "upload.txt") err := os.WriteFile(testFile, []byte("test content"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) @@ -140,15 +139,15 @@ func TestRunUpload_APIError(t *testing.T) { } err = runUpload(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to upload attachment") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to upload attachment") } func TestRunUpload_JSONOutput(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "upload.txt") err := os.WriteFile(testFile, []byte("test content"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -175,5 +174,5 @@ func TestRunUpload_JSONOutput(t *testing.T) { } err = runUpload(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } diff --git a/tools/cfl/internal/cmd/completion/completion_test.go b/tools/cfl/internal/cmd/completion/completion_test.go index 6558401..1eadcb5 100644 --- a/tools/cfl/internal/cmd/completion/completion_test.go +++ b/tools/cfl/internal/cmd/completion/completion_test.go @@ -5,8 +5,7 @@ import ( "testing" "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" ) @@ -34,12 +33,12 @@ func TestCompletionCommand(t *testing.T) { // Find the completion command completionCmd, _, err := rootCmd.Find([]string{"completion"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, "completion [bash|zsh|fish|powershell]", completionCmd.Use) - assert.NotEmpty(t, completionCmd.Short) - assert.NotEmpty(t, completionCmd.Long) - assert.Equal(t, []string{"bash", "zsh", "fish", "powershell"}, completionCmd.ValidArgs) + testutil.Equal(t, "completion [bash|zsh|fish|powershell]", completionCmd.Use) + testutil.NotEmpty(t, completionCmd.Short) + testutil.NotEmpty(t, completionCmd.Long) + testutil.Equal(t, []string{"bash", "zsh", "fish", "powershell"}, completionCmd.ValidArgs) } func TestBashCompletion(t *testing.T) { @@ -50,12 +49,12 @@ func TestBashCompletion(t *testing.T) { root.SetArgs([]string{"completion", "bash"}) err := root.Execute() - require.NoError(t, err) + testutil.RequireNoError(t, err) output := buf.String() - assert.NotEmpty(t, output) + testutil.NotEmpty(t, output) // Bash completions should contain bash-specific markers - assert.Contains(t, output, "bash completion") + testutil.Contains(t, output, "bash completion") } func TestZshCompletion(t *testing.T) { @@ -66,12 +65,12 @@ func TestZshCompletion(t *testing.T) { root.SetArgs([]string{"completion", "zsh"}) err := root.Execute() - require.NoError(t, err) + testutil.RequireNoError(t, err) output := buf.String() - assert.NotEmpty(t, output) + testutil.NotEmpty(t, output) // Zsh completions should contain zsh-specific markers - assert.Contains(t, output, "compdef") + testutil.Contains(t, output, "compdef") } func TestFishCompletion(t *testing.T) { @@ -82,12 +81,12 @@ func TestFishCompletion(t *testing.T) { root.SetArgs([]string{"completion", "fish"}) err := root.Execute() - require.NoError(t, err) + testutil.RequireNoError(t, err) output := buf.String() - assert.NotEmpty(t, output) + testutil.NotEmpty(t, output) // Fish completions should contain fish-specific markers - assert.Contains(t, output, "complete -c") + testutil.Contains(t, output, "complete -c") } func TestPowerShellCompletion(t *testing.T) { @@ -98,12 +97,12 @@ func TestPowerShellCompletion(t *testing.T) { root.SetArgs([]string{"completion", "powershell"}) err := root.Execute() - require.NoError(t, err) + testutil.RequireNoError(t, err) output := buf.String() - assert.NotEmpty(t, output) + testutil.NotEmpty(t, output) // PowerShell completions should contain PowerShell-specific markers - assert.Contains(t, output, "Register-ArgumentCompleter") + testutil.Contains(t, output, "Register-ArgumentCompleter") } func TestCompletionRequiresShellArg(t *testing.T) { @@ -113,8 +112,8 @@ func TestCompletionRequiresShellArg(t *testing.T) { root.SetErr(&bytes.Buffer{}) // Suppress error output err := root.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "accepts 1 arg(s)") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "accepts 1 arg(s)") } func TestCompletionRejectsInvalidShell(t *testing.T) { @@ -124,8 +123,8 @@ func TestCompletionRejectsInvalidShell(t *testing.T) { root.SetErr(&bytes.Buffer{}) // Suppress error output err := root.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid argument") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid argument") } func TestCompletionRejectsExtraArgs(t *testing.T) { @@ -135,6 +134,6 @@ func TestCompletionRejectsExtraArgs(t *testing.T) { root.SetErr(&bytes.Buffer{}) // Suppress error output err := root.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "accepts 1 arg(s)") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "accepts 1 arg(s)") } diff --git a/tools/cfl/internal/cmd/configcmd/clear_test.go b/tools/cfl/internal/cmd/configcmd/clear_test.go index 771152d..08f2ec4 100644 --- a/tools/cfl/internal/cmd/configcmd/clear_test.go +++ b/tools/cfl/internal/cmd/configcmd/clear_test.go @@ -7,8 +7,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" ) @@ -32,7 +31,7 @@ func TestRunClear_FileNotFound(t *testing.T) { } err := runClear(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunClear_WithForce(t *testing.T) { @@ -41,10 +40,10 @@ func TestRunClear_WithForce(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", tempDir) configDir := filepath.Join(tempDir, "cfl") - require.NoError(t, os.MkdirAll(configDir, 0755)) + testutil.RequireNoError(t, os.MkdirAll(configDir, 0755)) configPath := filepath.Join(configDir, "config.yml") err := os.WriteFile(configPath, []byte("url: https://test.atlassian.net"), 0600) - require.NoError(t, err) + testutil.RequireNoError(t, err) rootOpts := &root.Options{ Output: "table", @@ -60,11 +59,11 @@ func TestRunClear_WithForce(t *testing.T) { } err = runClear(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify file is deleted _, err = os.Stat(configPath) - assert.True(t, os.IsNotExist(err)) + testutil.True(t, os.IsNotExist(err)) } func TestRunClear_WithConfirmation(t *testing.T) { @@ -73,10 +72,10 @@ func TestRunClear_WithConfirmation(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", tempDir) configDir := filepath.Join(tempDir, "cfl") - require.NoError(t, os.MkdirAll(configDir, 0755)) + testutil.RequireNoError(t, os.MkdirAll(configDir, 0755)) configPath := filepath.Join(configDir, "config.yml") err := os.WriteFile(configPath, []byte("url: https://test.atlassian.net"), 0600) - require.NoError(t, err) + testutil.RequireNoError(t, err) rootOpts := &root.Options{ Output: "table", @@ -92,11 +91,11 @@ func TestRunClear_WithConfirmation(t *testing.T) { } err = runClear(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify file is deleted _, err = os.Stat(configPath) - assert.True(t, os.IsNotExist(err)) + testutil.True(t, os.IsNotExist(err)) } func TestRunClear_Cancelled(t *testing.T) { @@ -105,10 +104,10 @@ func TestRunClear_Cancelled(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", tempDir) configDir := filepath.Join(tempDir, "cfl") - require.NoError(t, os.MkdirAll(configDir, 0755)) + testutil.RequireNoError(t, os.MkdirAll(configDir, 0755)) configPath := filepath.Join(configDir, "config.yml") err := os.WriteFile(configPath, []byte("url: https://test.atlassian.net"), 0600) - require.NoError(t, err) + testutil.RequireNoError(t, err) rootOpts := &root.Options{ Output: "table", @@ -124,9 +123,9 @@ func TestRunClear_Cancelled(t *testing.T) { } err = runClear(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify file still exists _, err = os.Stat(configPath) - assert.NoError(t, err) + testutil.NoError(t, err) } diff --git a/tools/cfl/internal/cmd/configcmd/show_test.go b/tools/cfl/internal/cmd/configcmd/show_test.go index 30f8579..585b0df 100644 --- a/tools/cfl/internal/cmd/configcmd/show_test.go +++ b/tools/cfl/internal/cmd/configcmd/show_test.go @@ -3,7 +3,7 @@ package configcmd import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestMaskToken(t *testing.T) { @@ -42,7 +42,7 @@ func TestMaskToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := maskToken(tt.token) - assert.Equal(t, tt.want, got) + testutil.Equal(t, tt.want, got) }) } } @@ -85,8 +85,8 @@ func TestGetValueAndSource(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotValue, gotSource := getValueAndSource(tt.envValue, tt.fileValue, tt.envVarName) - assert.Equal(t, tt.wantValue, gotValue) - assert.Equal(t, tt.wantSource, gotSource) + testutil.Equal(t, tt.wantValue, gotValue) + testutil.Equal(t, tt.wantSource, gotSource) }) } } @@ -115,7 +115,7 @@ func TestFormatValueWithSource(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := formatValueWithSource(tt.value, tt.source) - assert.Equal(t, tt.want, got) + testutil.Equal(t, tt.want, got) }) } } diff --git a/tools/cfl/internal/cmd/configcmd/test_test.go b/tools/cfl/internal/cmd/configcmd/test_test.go index 03629d2..9e54e6b 100644 --- a/tools/cfl/internal/cmd/configcmd/test_test.go +++ b/tools/cfl/internal/cmd/configcmd/test_test.go @@ -7,8 +7,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -41,7 +40,7 @@ func TestRunTest_Success(t *testing.T) { rootOpts.SetAPIClient(client) err := runTest(rootOpts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Note: Output goes to real stdout via fmt.Print, not opts.Stdout // Just verifying no error is sufficient for this test } @@ -58,8 +57,8 @@ func TestRunTest_AuthFailure(t *testing.T) { rootOpts.SetAPIClient(client) err := runTest(rootOpts) - require.Error(t, err) - assert.Contains(t, err.Error(), "connection test failed") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "connection test failed") } func TestRunTest_ServerError(t *testing.T) { @@ -74,6 +73,6 @@ func TestRunTest_ServerError(t *testing.T) { rootOpts.SetAPIClient(client) err := runTest(rootOpts) - require.Error(t, err) - assert.Contains(t, err.Error(), "connection test failed") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "connection test failed") } diff --git a/tools/cfl/internal/cmd/init/init_test.go b/tools/cfl/internal/cmd/init/init_test.go index b1f0600..dd15933 100644 --- a/tools/cfl/internal/cmd/init/init_test.go +++ b/tools/cfl/internal/cmd/init/init_test.go @@ -9,8 +9,7 @@ import ( "testing" "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" "github.com/open-cli-collective/confluence-cli/internal/config" @@ -19,15 +18,15 @@ import ( func TestVerifyConnection_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify the request - assert.Equal(t, "/api/v2/spaces", r.URL.Path) - assert.Equal(t, "1", r.URL.Query().Get("limit")) - assert.Equal(t, "application/json", r.Header.Get("Accept")) + testutil.Equal(t, "/api/v2/spaces", r.URL.Path) + testutil.Equal(t, "1", r.URL.Query().Get("limit")) + testutil.Equal(t, "application/json", r.Header.Get("Accept")) // Verify basic auth is present user, pass, ok := r.BasicAuth() - assert.True(t, ok, "basic auth should be present") - assert.Equal(t, "test@example.com", user) - assert.Equal(t, "test-token", pass) + testutil.True(t, ok, "basic auth should be present") + testutil.Equal(t, "test@example.com", user) + testutil.Equal(t, "test-token", pass) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": []}`)) @@ -41,7 +40,7 @@ func TestVerifyConnection_Success(t *testing.T) { } err := verifyConnection(cfg) - assert.NoError(t, err) + testutil.NoError(t, err) } func TestVerifyConnection_Unauthorized(t *testing.T) { @@ -58,9 +57,9 @@ func TestVerifyConnection_Unauthorized(t *testing.T) { } err := verifyConnection(cfg) - require.Error(t, err) - assert.Contains(t, err.Error(), "authentication failed") - assert.Contains(t, err.Error(), "email and API token") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "authentication failed") + testutil.Contains(t, err.Error(), "email and API token") } func TestVerifyConnection_Forbidden(t *testing.T) { @@ -77,9 +76,9 @@ func TestVerifyConnection_Forbidden(t *testing.T) { } err := verifyConnection(cfg) - require.Error(t, err) - assert.Contains(t, err.Error(), "access denied") - assert.Contains(t, err.Error(), "permissions") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "access denied") + testutil.Contains(t, err.Error(), "permissions") } func TestVerifyConnection_ServerError(t *testing.T) { @@ -95,8 +94,8 @@ func TestVerifyConnection_ServerError(t *testing.T) { } err := verifyConnection(cfg) - require.Error(t, err) - assert.Contains(t, err.Error(), "unexpected status code: 500") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "unexpected status code: 500") } func TestVerifyConnection_NetworkError(t *testing.T) { @@ -107,7 +106,7 @@ func TestVerifyConnection_NetworkError(t *testing.T) { } err := verifyConnection(cfg) - require.Error(t, err) + testutil.RequireError(t, err) // Should fail to connect } @@ -170,10 +169,10 @@ func TestVerifyConnection_StatusCodes(t *testing.T) { err := verifyConnection(cfg) if tt.wantErr { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errContain) + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), tt.errContain) } else { - assert.NoError(t, err) + testutil.NoError(t, err) } }) } @@ -192,16 +191,16 @@ func TestConfigFilePermissions(t *testing.T) { // Save the config err := cfg.Save(configPath) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Check the file permissions info, err := os.Stat(configPath) - require.NoError(t, err) + testutil.RequireNoError(t, err) // On Unix, permissions should be 0600 (user read/write only) // The exact mode includes the file type bits, so we mask with 0777 perm := info.Mode().Perm() - assert.Equal(t, os.FileMode(0600), perm, "config file should have 0600 permissions") + testutil.Equal(t, perm, os.FileMode(0600)) } func TestConfigFilePermissions_DirectoryCreation(t *testing.T) { @@ -217,16 +216,16 @@ func TestConfigFilePermissions_DirectoryCreation(t *testing.T) { // Save should create the directory structure err := cfg.Save(configPath) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify file exists _, err = os.Stat(configPath) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify directory was created dirInfo, err := os.Stat(filepath.Dir(configPath)) - require.NoError(t, err) - assert.True(t, dirInfo.IsDir()) + testutil.RequireNoError(t, err) + testutil.True(t, dirInfo.IsDir()) } func TestInitCommand_Flags(t *testing.T) { @@ -247,23 +246,23 @@ func TestInitCommand_Flags(t *testing.T) { // Find the init command initCmd, _, err := rootCmd.Find([]string{"init"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify command structure - assert.Equal(t, "init", initCmd.Use) - assert.NotEmpty(t, initCmd.Short) - assert.NotEmpty(t, initCmd.Long) + testutil.Equal(t, "init", initCmd.Use) + testutil.NotEmpty(t, initCmd.Short) + testutil.NotEmpty(t, initCmd.Long) // Verify flags exist urlFlag := initCmd.Flags().Lookup("url") - require.NotNil(t, urlFlag) - assert.Equal(t, "", urlFlag.DefValue) + testutil.NotNil(t, urlFlag) + testutil.Equal(t, "", urlFlag.DefValue) emailFlag := initCmd.Flags().Lookup("email") - require.NotNil(t, emailFlag) - assert.Equal(t, "", emailFlag.DefValue) + testutil.NotNil(t, emailFlag) + testutil.Equal(t, "", emailFlag.DefValue) noVerifyFlag := initCmd.Flags().Lookup("no-verify") - require.NotNil(t, noVerifyFlag) - assert.Equal(t, "false", noVerifyFlag.DefValue) + testutil.NotNil(t, noVerifyFlag) + testutil.Equal(t, "false", noVerifyFlag.DefValue) } diff --git a/tools/cfl/internal/cmd/page/copy_test.go b/tools/cfl/internal/cmd/page/copy_test.go index 66cba62..6474d0e 100644 --- a/tools/cfl/internal/cmd/page/copy_test.go +++ b/tools/cfl/internal/cmd/page/copy_test.go @@ -7,8 +7,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -58,8 +57,8 @@ func newTestRootOptions() *root.Options { func TestRunCopy_Success(t *testing.T) { server := mockCopyServer(t, nil, func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "POST", r.Method) - assert.Equal(t, "/rest/api/content/12345/copy", r.URL.Path) + testutil.Equal(t, "POST", r.Method) + testutil.Equal(t, "/rest/api/content/12345/copy", r.URL.Path) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ "id": "99999", @@ -82,7 +81,7 @@ func TestRunCopy_Success(t *testing.T) { } err := runCopy("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunCopy_InfersSourceSpace(t *testing.T) { @@ -136,8 +135,8 @@ func TestRunCopy_InfersSourceSpace(t *testing.T) { } err := runCopy("12345", opts) - require.NoError(t, err) - assert.Equal(t, 3, callCount) // GetPage + GetSpace + CopyPage + testutil.RequireNoError(t, err) + testutil.Equal(t, 3, callCount) // GetPage + GetSpace + CopyPage } func TestRunCopy_PageNotFound(t *testing.T) { @@ -158,8 +157,8 @@ func TestRunCopy_PageNotFound(t *testing.T) { } err := runCopy("99999", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to copy page") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to copy page") } func TestRunCopy_JSONOutput(t *testing.T) { @@ -187,7 +186,7 @@ func TestRunCopy_JSONOutput(t *testing.T) { } err := runCopy("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunCopy_InvalidOutputFormat(t *testing.T) { @@ -203,8 +202,8 @@ func TestRunCopy_InvalidOutputFormat(t *testing.T) { } err := runCopy("12345", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid output format") } func TestRunCopy_GetSourcePageFails(t *testing.T) { @@ -228,8 +227,8 @@ func TestRunCopy_GetSourcePageFails(t *testing.T) { } err := runCopy("invalid", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get source page") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to get source page") } func TestRunCopy_WithNoAttachments(t *testing.T) { @@ -257,7 +256,7 @@ func TestRunCopy_WithNoAttachments(t *testing.T) { } err := runCopy("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunCopy_WithNoLabels(t *testing.T) { @@ -285,7 +284,7 @@ func TestRunCopy_WithNoLabels(t *testing.T) { } err := runCopy("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunCopy_PermissionDenied(t *testing.T) { @@ -306,8 +305,8 @@ func TestRunCopy_PermissionDenied(t *testing.T) { } err := runCopy("12345", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to copy page") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to copy page") } func TestRunCopy_GetSpaceFails(t *testing.T) { @@ -343,6 +342,6 @@ func TestRunCopy_GetSpaceFails(t *testing.T) { } err := runCopy("12345", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get space") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to get space") } diff --git a/tools/cfl/internal/cmd/page/create_test.go b/tools/cfl/internal/cmd/page/create_test.go index 6ad526e..d5d189c 100644 --- a/tools/cfl/internal/cmd/page/create_test.go +++ b/tools/cfl/internal/cmd/page/create_test.go @@ -11,8 +11,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -62,7 +61,7 @@ func TestRunCreate_Success(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Hello\n\nWorld"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := mockCreateServer(t, "DEV", "123456", http.StatusOK) defer server.Close() @@ -79,7 +78,7 @@ func TestRunCreate_Success(t *testing.T) { } err = runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunCreate_HTMLFile_Legacy(t *testing.T) { @@ -87,7 +86,7 @@ func TestRunCreate_HTMLFile_Legacy(t *testing.T) { tmpDir := t.TempDir() htmlFile := filepath.Join(tmpDir, "content.html") err := os.WriteFile(htmlFile, []byte("

Hello World

"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -119,13 +118,13 @@ func TestRunCreate_HTMLFile_Legacy(t *testing.T) { } err = runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify HTML was not converted (should be passed as-is in storage format) bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) content := storageMap["value"].(string) - assert.Equal(t, "

Hello World

", content) + testutil.Equal(t, "

Hello World

", content) } func TestRunCreate_NoMarkdownFlag_Legacy(t *testing.T) { @@ -133,7 +132,7 @@ func TestRunCreate_NoMarkdownFlag_Legacy(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("

Raw XHTML

"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -167,20 +166,20 @@ func TestRunCreate_NoMarkdownFlag_Legacy(t *testing.T) { } err = runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify content was not converted even though file has .md extension bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) content := storageMap["value"].(string) - assert.Equal(t, "

Raw XHTML

", content) + testutil.Equal(t, "

Raw XHTML

", content) } func TestRunCreate_MissingSpace(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Hello"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Don't need server - should fail before API call rootOpts := newCreateTestRootOptions() @@ -195,15 +194,15 @@ func TestRunCreate_MissingSpace(t *testing.T) { } err = runCreate(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "space is required") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "space is required") } func TestRunCreate_SpaceNotFound(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Hello"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Return empty results for space lookup @@ -224,15 +223,15 @@ func TestRunCreate_SpaceNotFound(t *testing.T) { } err = runCreate(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to find space") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to find space") } func TestRunCreate_CreateFailed(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Hello"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := mockCreateServer(t, "DEV", "123456", http.StatusForbidden) defer server.Close() @@ -249,15 +248,15 @@ func TestRunCreate_CreateFailed(t *testing.T) { } err = runCreate(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to create page") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to create page") } func TestRunCreate_WithParent(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Child Page"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -289,17 +288,17 @@ func TestRunCreate_WithParent(t *testing.T) { } err = runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify parent ID was included in request - assert.Equal(t, "12345", receivedBody["parentId"]) + testutil.Equal(t, "12345", receivedBody["parentId"]) } func TestRunCreate_JSONOutput(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Hello"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := mockCreateServer(t, "DEV", "123456", http.StatusOK) defer server.Close() @@ -317,14 +316,14 @@ func TestRunCreate_JSONOutput(t *testing.T) { } err = runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunCreate_MarkdownConversion_Legacy(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Hello World\n\nThis is **bold** text."), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -356,7 +355,7 @@ func TestRunCreate_MarkdownConversion_Legacy(t *testing.T) { } err = runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify markdown was converted to HTML storage format bodyMap := receivedBody["body"].(map[string]interface{}) @@ -364,15 +363,15 @@ func TestRunCreate_MarkdownConversion_Legacy(t *testing.T) { content := storageMap["value"].(string) // Should have HTML heading and strong tag from markdown conversion - assert.Contains(t, content, "bold") + testutil.Contains(t, content, "bold") } func TestRunCreate_MarkdownToADF(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Hello World\n\nThis is **bold** text."), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -404,7 +403,7 @@ func TestRunCreate_MarkdownToADF(t *testing.T) { } err = runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify ADF format was used (default) bodyMap := receivedBody["body"].(map[string]interface{}) @@ -412,9 +411,9 @@ func TestRunCreate_MarkdownToADF(t *testing.T) { content := adfMap["value"].(string) // Should be valid ADF JSON with heading and strong mark - assert.Contains(t, content, `"type":"doc"`) - assert.Contains(t, content, `"type":"heading"`) - assert.Contains(t, content, `"type":"strong"`) + testutil.Contains(t, content, `"type":"doc"`) + testutil.Contains(t, content, `"type":"heading"`) + testutil.Contains(t, content, `"type":"strong"`) } func TestRunCreate_FileReadError(t *testing.T) { @@ -433,8 +432,8 @@ func TestRunCreate_FileReadError(t *testing.T) { } err := runCreate(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to read file") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to read file") } func TestRunCreate_Stdin_ADF(t *testing.T) { @@ -467,16 +466,16 @@ func TestRunCreate_Stdin_ADF(t *testing.T) { } err := runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify ADF format was used bodyMap := receivedBody["body"].(map[string]interface{}) adfMap := bodyMap["atlas_doc_format"].(map[string]interface{}) content := adfMap["value"].(string) - assert.Contains(t, content, `"type":"doc"`) - assert.Contains(t, content, `"type":"heading"`) - assert.Contains(t, content, `"type":"strong"`) + testutil.Contains(t, content, `"type":"doc"`) + testutil.Contains(t, content, `"type":"heading"`) + testutil.Contains(t, content, `"type":"strong"`) } func TestRunCreate_Stdin_Legacy(t *testing.T) { @@ -510,15 +509,15 @@ func TestRunCreate_Stdin_Legacy(t *testing.T) { } err := runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify storage format was used bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) content := storageMap["value"].(string) - assert.Contains(t, content, "bold") + testutil.Contains(t, content, "bold") } func TestRunCreate_Stdin_NoMarkdown_Legacy(t *testing.T) { @@ -554,14 +553,14 @@ func TestRunCreate_Stdin_NoMarkdown_Legacy(t *testing.T) { } err := runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify raw content passed through without conversion bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) content := storageMap["value"].(string) - assert.Equal(t, "

Raw XHTML content

", content) + testutil.Equal(t, "

Raw XHTML content

", content) } func TestRunCreate_StorageFlag_Stdin(t *testing.T) { @@ -597,7 +596,7 @@ func TestRunCreate_StorageFlag_Stdin(t *testing.T) { } err := runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify storage format was used (not atlas_doc_format) bodyMap := receivedBody["body"].(map[string]interface{}) @@ -605,15 +604,15 @@ func TestRunCreate_StorageFlag_Stdin(t *testing.T) { content := storageMap["value"].(string) // Content should be passed through as-is - assert.Contains(t, content, `ac:structured-macro`) - assert.Nil(t, bodyMap["atlas_doc_format"]) + testutil.Contains(t, content, `ac:structured-macro`) + testutil.Nil(t, bodyMap["atlas_doc_format"]) } func TestRunCreate_StorageFlag_File(t *testing.T) { tmpDir := t.TempDir() htmlFile := filepath.Join(tmpDir, "content.html") err := os.WriteFile(htmlFile, []byte("

Direct storage XHTML

"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -647,14 +646,14 @@ func TestRunCreate_StorageFlag_File(t *testing.T) { } err = runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify storage format was used without --legacy bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) content := storageMap["value"].(string) - assert.Equal(t, "

Direct storage XHTML

", content) - assert.Nil(t, bodyMap["atlas_doc_format"]) + testutil.Equal(t, "

Direct storage XHTML

", content) + testutil.Nil(t, bodyMap["atlas_doc_format"]) } func TestRunCreate_ComplexMarkdown_ADF(t *testing.T) { @@ -699,17 +698,17 @@ func TestRunCreate_ComplexMarkdown_ADF(t *testing.T) { } err := runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify ADF contains complex elements bodyMap := receivedBody["body"].(map[string]interface{}) adfMap := bodyMap["atlas_doc_format"].(map[string]interface{}) content := adfMap["value"].(string) - assert.Contains(t, content, `"type":"table"`) - assert.Contains(t, content, `"type":"bulletList"`) - assert.Contains(t, content, `"type":"codeBlock"`) - assert.Contains(t, content, `"language":"go"`) + testutil.Contains(t, content, `"type":"table"`) + testutil.Contains(t, content, `"type":"bulletList"`) + testutil.Contains(t, content, `"type":"codeBlock"`) + testutil.Contains(t, content, `"language":"go"`) } func TestRunCreate_EmptyContentFromStdin(t *testing.T) { @@ -728,8 +727,8 @@ func TestRunCreate_EmptyContentFromStdin(t *testing.T) { } err := runCreate(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "page content cannot be empty") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "page content cannot be empty") } func TestRunCreate_WhitespaceOnlyFromStdin(t *testing.T) { @@ -748,15 +747,15 @@ func TestRunCreate_WhitespaceOnlyFromStdin(t *testing.T) { } err := runCreate(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "page content cannot be empty") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "page content cannot be empty") } func TestRunCreate_EmptyFile(t *testing.T) { tmpDir := t.TempDir() emptyFile := filepath.Join(tmpDir, "empty.md") err := os.WriteFile(emptyFile, []byte(""), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := mockCreateServer(t, "DEV", "123456", http.StatusOK) defer server.Close() @@ -773,15 +772,15 @@ func TestRunCreate_EmptyFile(t *testing.T) { } err = runCreate(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "page content cannot be empty") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "page content cannot be empty") } func TestRunCreate_WhitespaceOnlyFile(t *testing.T) { tmpDir := t.TempDir() whitespaceFile := filepath.Join(tmpDir, "whitespace.md") err := os.WriteFile(whitespaceFile, []byte(" \n\t\n "), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := mockCreateServer(t, "DEV", "123456", http.StatusOK) defer server.Close() @@ -798,6 +797,6 @@ func TestRunCreate_WhitespaceOnlyFile(t *testing.T) { } err = runCreate(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "page content cannot be empty") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "page content cannot be empty") } diff --git a/tools/cfl/internal/cmd/page/delete_test.go b/tools/cfl/internal/cmd/page/delete_test.go index 6e58f56..10e9120 100644 --- a/tools/cfl/internal/cmd/page/delete_test.go +++ b/tools/cfl/internal/cmd/page/delete_test.go @@ -7,8 +7,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -60,7 +59,7 @@ func TestRunDelete_ConfirmYes(t *testing.T) { } err := runDelete("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunDelete_ConfirmYesUppercase(t *testing.T) { @@ -78,7 +77,7 @@ func TestRunDelete_ConfirmYesUppercase(t *testing.T) { } err := runDelete("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunDelete_ConfirmNo(t *testing.T) { @@ -96,7 +95,7 @@ func TestRunDelete_ConfirmNo(t *testing.T) { } err := runDelete("12345", opts) - require.NoError(t, err) // Cancellation is not an error + testutil.RequireNoError(t, err) // Cancellation is not an error } func TestRunDelete_ConfirmEmpty(t *testing.T) { @@ -114,7 +113,7 @@ func TestRunDelete_ConfirmEmpty(t *testing.T) { } err := runDelete("12345", opts) - require.NoError(t, err) // Empty input should cancel + testutil.RequireNoError(t, err) // Empty input should cancel } func TestRunDelete_ConfirmOther(t *testing.T) { @@ -132,7 +131,7 @@ func TestRunDelete_ConfirmOther(t *testing.T) { } err := runDelete("12345", opts) - require.NoError(t, err) // Any non-y/Y input should cancel + testutil.RequireNoError(t, err) // Any non-y/Y input should cancel } func TestRunDelete_Force(t *testing.T) { @@ -149,7 +148,7 @@ func TestRunDelete_Force(t *testing.T) { } err := runDelete("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunDelete_PageNotFound(t *testing.T) { @@ -169,8 +168,8 @@ func TestRunDelete_PageNotFound(t *testing.T) { } err := runDelete("99999", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get page") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to get page") } func TestRunDelete_DeleteFailed(t *testing.T) { @@ -187,8 +186,8 @@ func TestRunDelete_DeleteFailed(t *testing.T) { } err := runDelete("12345", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to delete page") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to delete page") } func TestRunDelete_JSONOutput(t *testing.T) { @@ -206,7 +205,7 @@ func TestRunDelete_JSONOutput(t *testing.T) { } err := runDelete("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunDelete_ConfirmationInputs(t *testing.T) { @@ -251,8 +250,8 @@ func TestRunDelete_ConfirmationInputs(t *testing.T) { } err := runDelete("12345", opts) - require.NoError(t, err) - assert.Equal(t, tt.shouldProceed, deleteCalled, "delete should have been called: %v", tt.shouldProceed) + testutil.RequireNoError(t, err) + testutil.Equal(t, deleteCalled, tt.shouldProceed) }) } } diff --git a/tools/cfl/internal/cmd/page/edit_test.go b/tools/cfl/internal/cmd/page/edit_test.go index 2c2defc..92f0b81 100644 --- a/tools/cfl/internal/cmd/page/edit_test.go +++ b/tools/cfl/internal/cmd/page/edit_test.go @@ -11,8 +11,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -31,7 +30,7 @@ func TestRunEdit_Success(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Updated Content\n\nNew text here."), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { @@ -69,7 +68,7 @@ func TestRunEdit_Success(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunEdit_TitleOnly(t *testing.T) { @@ -117,17 +116,17 @@ func TestRunEdit_TitleOnly(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("

Keep this

"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) useMd := false opts.file = mdFile opts.markdown = &useMd err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify title was changed - assert.Equal(t, "New Title", receivedBody["title"]) + testutil.Equal(t, "New Title", receivedBody["title"]) } func TestRunEdit_PageNotFound(t *testing.T) { @@ -148,15 +147,15 @@ func TestRunEdit_PageNotFound(t *testing.T) { } err := runEdit(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get page") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to get page") } func TestRunEdit_UpdateFailed(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# New Content"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { @@ -189,15 +188,15 @@ func TestRunEdit_UpdateFailed(t *testing.T) { } err = runEdit(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to update page") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to update page") } func TestRunEdit_VersionIncrement(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Updated"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedVersion int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -242,17 +241,17 @@ func TestRunEdit_VersionIncrement(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify version was incremented from 7 to 8 - assert.Equal(t, 8, receivedVersion) + testutil.Equal(t, 8, receivedVersion) } func TestRunEdit_HTMLFile(t *testing.T) { tmpDir := t.TempDir() htmlFile := filepath.Join(tmpDir, "content.html") err := os.WriteFile(htmlFile, []byte("

Direct HTML

"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -295,20 +294,20 @@ func TestRunEdit_HTMLFile(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify HTML was not converted (storage format in legacy mode) bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) content := storageMap["value"].(string) - assert.Equal(t, "

Direct HTML

", content) + testutil.Equal(t, "

Direct HTML

", content) } func TestRunEdit_NoMarkdownFlag(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("

Raw XHTML in .md file

"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -352,20 +351,20 @@ func TestRunEdit_NoMarkdownFlag(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify content was not converted (storage format in legacy mode) bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) content := storageMap["value"].(string) - assert.Equal(t, "

Raw XHTML in .md file

", content) + testutil.Equal(t, "

Raw XHTML in .md file

", content) } func TestRunEdit_MarkdownToADF(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Updated\n\nNew **bold** text."), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -408,7 +407,7 @@ func TestRunEdit_MarkdownToADF(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify ADF format was used (default) bodyMap := receivedBody["body"].(map[string]interface{}) @@ -416,16 +415,16 @@ func TestRunEdit_MarkdownToADF(t *testing.T) { content := adfMap["value"].(string) // Should be valid ADF JSON - assert.Contains(t, content, `"type":"doc"`) - assert.Contains(t, content, `"type":"heading"`) - assert.Contains(t, content, `"type":"strong"`) + testutil.Contains(t, content, `"type":"doc"`) + testutil.Contains(t, content, `"type":"heading"`) + testutil.Contains(t, content, `"type":"strong"`) } func TestRunEdit_JSONOutput(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Updated"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { @@ -463,7 +462,7 @@ func TestRunEdit_JSONOutput(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunEdit_FileReadError(t *testing.T) { @@ -490,8 +489,8 @@ func TestRunEdit_FileReadError(t *testing.T) { } err := runEdit(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to read file") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to read file") } func TestRunEdit_Stdin_ADF(t *testing.T) { @@ -533,16 +532,16 @@ func TestRunEdit_Stdin_ADF(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify ADF format was used bodyMap := receivedBody["body"].(map[string]interface{}) adfMap := bodyMap["atlas_doc_format"].(map[string]interface{}) content := adfMap["value"].(string) - assert.Contains(t, content, `"type":"doc"`) - assert.Contains(t, content, `"type":"heading"`) - assert.Contains(t, content, `"type":"strong"`) + testutil.Contains(t, content, `"type":"doc"`) + testutil.Contains(t, content, `"type":"heading"`) + testutil.Contains(t, content, `"type":"strong"`) } func TestRunEdit_Stdin_Legacy(t *testing.T) { @@ -585,15 +584,15 @@ func TestRunEdit_Stdin_Legacy(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify storage format was used bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) content := storageMap["value"].(string) - assert.Contains(t, content, "bold") + testutil.Contains(t, content, "bold") } func TestRunEdit_TitleAndContent(t *testing.T) { @@ -628,7 +627,7 @@ func TestRunEdit_TitleAndContent(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# New Content\n\nUpdated text here."), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) rootOpts := newEditTestRootOptions() client := api.NewClient(server.URL, "test@example.com", "token") @@ -641,13 +640,13 @@ func TestRunEdit_TitleAndContent(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify both title and content were updated - assert.Equal(t, "New Title", receivedBody["title"]) + testutil.Equal(t, "New Title", receivedBody["title"]) bodyMap := receivedBody["body"].(map[string]interface{}) adfMap := bodyMap["atlas_doc_format"].(map[string]interface{}) - assert.NotNil(t, adfMap["value"]) + testutil.NotNil(t, adfMap["value"]) } func TestRunEdit_ComplexMarkdown_ADF(t *testing.T) { @@ -694,7 +693,7 @@ func TestRunEdit_ComplexMarkdown_ADF(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "complex.md") err := os.WriteFile(mdFile, []byte(complexMarkdown), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) rootOpts := newEditTestRootOptions() client := api.NewClient(server.URL, "test@example.com", "token") @@ -706,17 +705,17 @@ func TestRunEdit_ComplexMarkdown_ADF(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify ADF contains complex elements bodyMap := receivedBody["body"].(map[string]interface{}) adfMap := bodyMap["atlas_doc_format"].(map[string]interface{}) content := adfMap["value"].(string) - assert.Contains(t, content, `"type":"table"`) - assert.Contains(t, content, `"type":"bulletList"`) - assert.Contains(t, content, `"type":"codeBlock"`) - assert.Contains(t, content, `"language":"go"`) + testutil.Contains(t, content, `"type":"table"`) + testutil.Contains(t, content, `"type":"bulletList"`) + testutil.Contains(t, content, `"type":"codeBlock"`) + testutil.Contains(t, content, `"language":"go"`) } func TestRunEdit_MoveToParent(t *testing.T) { @@ -762,8 +761,8 @@ func TestRunEdit_MoveToParent(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) - assert.True(t, moveCalled, "MovePage should have been called") + testutil.RequireNoError(t, err) + testutil.True(t, moveCalled, "MovePage should have been called") } func TestRunEdit_MoveAndRename(t *testing.T) { @@ -814,9 +813,9 @@ func TestRunEdit_MoveAndRename(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) - assert.True(t, moveCalled, "MovePage should have been called") - assert.Equal(t, "New Title", receivedTitle) + testutil.RequireNoError(t, err) + testutil.True(t, moveCalled, "MovePage should have been called") + testutil.Equal(t, "New Title", receivedTitle) } func TestRunEdit_MoveFailed(t *testing.T) { @@ -861,8 +860,8 @@ func TestRunEdit_MoveFailed(t *testing.T) { } err := runEdit(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to move page to new parent") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to move page to new parent") } func TestRunEdit_MoveWithContent(t *testing.T) { @@ -910,13 +909,13 @@ func TestRunEdit_MoveWithContent(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) - assert.True(t, moveCalled, "MovePage should have been called") + testutil.RequireNoError(t, err) + testutil.True(t, moveCalled, "MovePage should have been called") // Verify content was also updated bodyMap := receivedBody["body"].(map[string]interface{}) adfMap := bodyMap["atlas_doc_format"].(map[string]interface{}) - assert.NotNil(t, adfMap["value"]) + testutil.NotNil(t, adfMap["value"]) } func TestRunEdit_EmptyContentFromStdin(t *testing.T) { @@ -946,8 +945,8 @@ func TestRunEdit_EmptyContentFromStdin(t *testing.T) { } err := runEdit(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "page content cannot be empty") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "page content cannot be empty") } func TestRunEdit_WhitespaceOnlyFromStdin(t *testing.T) { @@ -977,15 +976,15 @@ func TestRunEdit_WhitespaceOnlyFromStdin(t *testing.T) { } err := runEdit(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "page content cannot be empty") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "page content cannot be empty") } func TestRunEdit_EmptyFile(t *testing.T) { tmpDir := t.TempDir() emptyFile := filepath.Join(tmpDir, "empty.md") err := os.WriteFile(emptyFile, []byte(""), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { @@ -1014,15 +1013,15 @@ func TestRunEdit_EmptyFile(t *testing.T) { } err = runEdit(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "page content cannot be empty") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "page content cannot be empty") } func TestRunEdit_WhitespaceOnlyFile(t *testing.T) { tmpDir := t.TempDir() whitespaceFile := filepath.Join(tmpDir, "whitespace.md") err := os.WriteFile(whitespaceFile, []byte(" \n\t\n "), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { @@ -1051,8 +1050,8 @@ func TestRunEdit_WhitespaceOnlyFile(t *testing.T) { } err = runEdit(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "page content cannot be empty") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "page content cannot be empty") } func TestRunEdit_TitleOnlyUpdate_NoContentValidation(t *testing.T) { @@ -1092,7 +1091,7 @@ func TestRunEdit_TitleOnlyUpdate_NoContentValidation(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Valid Content"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) rootOpts.Stdin = nil opts := &editOptions{ @@ -1103,8 +1102,8 @@ func TestRunEdit_TitleOnlyUpdate_NoContentValidation(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) - assert.True(t, updateCalled, "Update should have been called") + testutil.RequireNoError(t, err) + testutil.True(t, updateCalled, "Update should have been called") } func TestRunEdit_MoveOnly_NoEditorOpened(t *testing.T) { @@ -1153,9 +1152,9 @@ func TestRunEdit_MoveOnly_NoEditorOpened(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) - assert.True(t, updateCalled, "UpdatePage should have been called") - assert.True(t, moveCalled, "MovePage should have been called") + testutil.RequireNoError(t, err) + testutil.True(t, updateCalled, "UpdatePage should have been called") + testutil.True(t, moveCalled, "MovePage should have been called") } func TestRunEdit_MoveWithTitleOnly_NoEditorOpened(t *testing.T) { @@ -1206,9 +1205,9 @@ func TestRunEdit_MoveWithTitleOnly_NoEditorOpened(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) - assert.True(t, moveCalled, "MovePage should have been called") - assert.Equal(t, "New Title", receivedBody["title"]) + testutil.RequireNoError(t, err) + testutil.True(t, moveCalled, "MovePage should have been called") + testutil.Equal(t, "New Title", receivedBody["title"]) } func TestRunEdit_StorageFlag_Stdin(t *testing.T) { @@ -1253,7 +1252,7 @@ func TestRunEdit_StorageFlag_Stdin(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify storage format was used (not atlas_doc_format) bodyMap := receivedBody["body"].(map[string]interface{}) @@ -1261,18 +1260,18 @@ func TestRunEdit_StorageFlag_Stdin(t *testing.T) { content := storageMap["value"].(string) // Content should be passed through as-is, preserving Confluence-specific markup - assert.Contains(t, content, `ac:structured-macro`) - assert.Contains(t, content, `ri:user`) + testutil.Contains(t, content, `ac:structured-macro`) + testutil.Contains(t, content, `ri:user`) // Should NOT have atlas_doc_format - assert.Nil(t, bodyMap["atlas_doc_format"]) + testutil.Nil(t, bodyMap["atlas_doc_format"]) } func TestRunEdit_StorageFlag_File(t *testing.T) { tmpDir := t.TempDir() htmlFile := filepath.Join(tmpDir, "content.html") err := os.WriteFile(htmlFile, []byte("

Direct storage XHTML

"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1316,16 +1315,16 @@ func TestRunEdit_StorageFlag_File(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify storage format was used without --legacy bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) content := storageMap["value"].(string) - assert.Equal(t, "

Direct storage XHTML

", content) + testutil.Equal(t, "

Direct storage XHTML

", content) // Should NOT have atlas_doc_format - assert.Nil(t, bodyMap["atlas_doc_format"]) + testutil.Nil(t, bodyMap["atlas_doc_format"]) } func TestRunEdit_MoveOnly_BodyPreserved(t *testing.T) { @@ -1373,12 +1372,12 @@ func TestRunEdit_MoveOnly_BodyPreserved(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify body was preserved from original page bodyMap := receivedBody["body"].(map[string]interface{}) storageMap := bodyMap["storage"].(map[string]interface{}) - assert.Equal(t, "

Original content that must be preserved

", storageMap["value"]) + testutil.Equal(t, "

Original content that must be preserved

", storageMap["value"]) } func TestRunEdit_ADFPage_TitleOnly_PreservesBody(t *testing.T) { @@ -1436,19 +1435,19 @@ func TestRunEdit_ADFPage_TitleOnly_PreservesBody(t *testing.T) { } err := runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify body was preserved as ADF (not storage) bodyMap := receivedBody["body"].(map[string]interface{}) adfMap := bodyMap["atlas_doc_format"].(map[string]interface{}) - assert.Contains(t, adfMap["value"], "ADF body") + testutil.Contains(t, adfMap["value"].(string), "ADF body") } func TestRunEdit_ADFPage_NewContent(t *testing.T) { tmpDir := t.TempDir() mdFile := filepath.Join(tmpDir, "content.md") err := os.WriteFile(mdFile, []byte("# Updated Content"), 0644) - require.NoError(t, err) + testutil.RequireNoError(t, err) var receivedBody map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1502,10 +1501,10 @@ func TestRunEdit_ADFPage_NewContent(t *testing.T) { } err = runEdit(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // New content should be submitted as ADF (default path) bodyMap := receivedBody["body"].(map[string]interface{}) adfMap := bodyMap["atlas_doc_format"].(map[string]interface{}) - assert.Contains(t, adfMap["value"], "Updated Content") + testutil.Contains(t, adfMap["value"].(string), "Updated Content") } diff --git a/tools/cfl/internal/cmd/page/fetch_test.go b/tools/cfl/internal/cmd/page/fetch_test.go index c5b9168..1a8e8e5 100644 --- a/tools/cfl/internal/cmd/page/fetch_test.go +++ b/tools/cfl/internal/cmd/page/fetch_test.go @@ -7,8 +7,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" ) @@ -17,7 +16,7 @@ func TestGetPageWithBodyFallback_StorageHasContent(t *testing.T) { callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ - assert.Equal(t, "storage", r.URL.Query().Get("body-format"), "should only request storage") + testutil.Equal(t, "storage", r.URL.Query().Get("body-format")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ "id": "12345", @@ -31,9 +30,9 @@ func TestGetPageWithBodyFallback_StorageHasContent(t *testing.T) { client := api.NewClient(server.URL, "test@example.com", "token") page, err := getPageWithBodyFallback(context.Background(), client, "12345") - require.NoError(t, err) - assert.Equal(t, 1, callCount, "should not make a second call when storage has content") - assert.True(t, hasStorageContent(page)) + testutil.RequireNoError(t, err) + testutil.Equal(t, 1, callCount) + testutil.True(t, hasStorageContent(page)) } func TestGetPageWithBodyFallback_StorageEmpty_FallsBackToADF(t *testing.T) { @@ -65,9 +64,9 @@ func TestGetPageWithBodyFallback_StorageEmpty_FallsBackToADF(t *testing.T) { client := api.NewClient(server.URL, "test@example.com", "token") page, err := getPageWithBodyFallback(context.Background(), client, "12345") - require.NoError(t, err) - assert.Equal(t, 2, callCount, "should make fallback call when storage is empty") - assert.True(t, hasADFContent(page)) + testutil.RequireNoError(t, err) + testutil.Equal(t, 2, callCount) + testutil.True(t, hasADFContent(page)) } func TestGetPageWithBodyFallback_NullBody_FallsBackToADF(t *testing.T) { @@ -99,9 +98,9 @@ func TestGetPageWithBodyFallback_NullBody_FallsBackToADF(t *testing.T) { client := api.NewClient(server.URL, "test@example.com", "token") page, err := getPageWithBodyFallback(context.Background(), client, "12345") - require.NoError(t, err) - assert.Equal(t, 2, callCount) - assert.True(t, hasADFContent(page)) + testutil.RequireNoError(t, err) + testutil.Equal(t, 2, callCount) + testutil.True(t, hasADFContent(page)) } func TestGetPageWithBodyFallback_BothEmpty(t *testing.T) { @@ -119,9 +118,9 @@ func TestGetPageWithBodyFallback_BothEmpty(t *testing.T) { client := api.NewClient(server.URL, "test@example.com", "token") page, err := getPageWithBodyFallback(context.Background(), client, "12345") - require.NoError(t, err) - assert.False(t, hasStorageContent(page)) - assert.False(t, hasADFContent(page)) + testutil.RequireNoError(t, err) + testutil.False(t, hasStorageContent(page)) + testutil.False(t, hasADFContent(page)) } func TestGetPageWithBodyFallback_GetPageError(t *testing.T) { @@ -133,7 +132,7 @@ func TestGetPageWithBodyFallback_GetPageError(t *testing.T) { client := api.NewClient(server.URL, "test@example.com", "token") _, err := getPageWithBodyFallback(context.Background(), client, "99999") - require.Error(t, err) + testutil.RequireError(t, err) } func TestGetPageWithBodyFallback_ADFFallbackFails_GracefulDegradation(t *testing.T) { @@ -160,9 +159,9 @@ func TestGetPageWithBodyFallback_ADFFallbackFails_GracefulDegradation(t *testing client := api.NewClient(server.URL, "test@example.com", "token") page, err := getPageWithBodyFallback(context.Background(), client, "12345") - require.NoError(t, err, "should not error even if ADF fallback fails") - assert.False(t, hasStorageContent(page)) - assert.False(t, hasADFContent(page)) + testutil.RequireNoError(t, err) + testutil.False(t, hasStorageContent(page)) + testutil.False(t, hasADFContent(page)) } func TestHasStorageContent(t *testing.T) { @@ -179,7 +178,7 @@ func TestHasStorageContent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, hasStorageContent(tt.page)) + testutil.Equal(t, tt.expected, hasStorageContent(tt.page)) }) } } @@ -198,7 +197,7 @@ func TestHasADFContent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, hasADFContent(tt.page)) + testutil.Equal(t, tt.expected, hasADFContent(tt.page)) }) } } diff --git a/tools/cfl/internal/cmd/page/list_test.go b/tools/cfl/internal/cmd/page/list_test.go index c39bcb2..0a66cc9 100644 --- a/tools/cfl/internal/cmd/page/list_test.go +++ b/tools/cfl/internal/cmd/page/list_test.go @@ -7,8 +7,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -66,7 +65,7 @@ func TestRunList_PageList_Success(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_PageList_EmptyResults(t *testing.T) { @@ -85,7 +84,7 @@ func TestRunList_PageList_EmptyResults(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_PageList_JSONOutput(t *testing.T) { @@ -109,7 +108,7 @@ func TestRunList_PageList_JSONOutput(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_PageList_InvalidOutputFormat(t *testing.T) { @@ -123,8 +122,8 @@ func TestRunList_PageList_InvalidOutputFormat(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid output format") } func TestRunList_PageList_NegativeLimit(t *testing.T) { @@ -138,8 +137,8 @@ func TestRunList_PageList_NegativeLimit(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid limit") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid limit") } func TestRunList_PageList_ZeroLimit(t *testing.T) { @@ -154,7 +153,7 @@ func TestRunList_PageList_ZeroLimit(t *testing.T) { // Zero limit should return empty without making API call err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_PageList_MissingSpace(t *testing.T) { @@ -176,8 +175,8 @@ func TestRunList_PageList_MissingSpace(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "space is required") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "space is required") } func TestRunList_PageList_SpaceNotFound(t *testing.T) { @@ -200,8 +199,8 @@ func TestRunList_PageList_SpaceNotFound(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to find space") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to find space") } func TestRunList_PageList_NullVersion(t *testing.T) { @@ -224,7 +223,7 @@ func TestRunList_PageList_NullVersion(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_PageList_HasMore(t *testing.T) { @@ -248,7 +247,7 @@ func TestRunList_PageList_HasMore(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_PageList_LongTitle(t *testing.T) { @@ -272,13 +271,13 @@ func TestRunList_PageList_LongTitle(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_PageList_StatusFilter(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/pages") { - assert.Equal(t, "archived", r.URL.Query().Get("status")) + testutil.Equal(t, "archived", r.URL.Query().Get("status")) } if r.URL.Query().Get("keys") != "" { w.WriteHeader(http.StatusOK) @@ -302,7 +301,7 @@ func TestRunList_PageList_StatusFilter(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_PageList_InvalidStatus(t *testing.T) { @@ -316,15 +315,15 @@ func TestRunList_PageList_InvalidStatus(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid status") - assert.Contains(t, err.Error(), "draft") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid status") + testutil.Contains(t, err.Error(), "draft") } func TestRunList_PageList_TrashedStatus(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/pages") { - assert.Equal(t, "trashed", r.URL.Query().Get("status")) + testutil.Equal(t, "trashed", r.URL.Query().Get("status")) } if r.URL.Query().Get("keys") != "" { w.WriteHeader(http.StatusOK) @@ -348,5 +347,5 @@ func TestRunList_PageList_TrashedStatus(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } diff --git a/tools/cfl/internal/cmd/page/view_test.go b/tools/cfl/internal/cmd/page/view_test.go index 7803c69..535fa25 100644 --- a/tools/cfl/internal/cmd/page/view_test.go +++ b/tools/cfl/internal/cmd/page/view_test.go @@ -8,8 +8,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -26,9 +25,9 @@ func newViewTestRootOptions() *root.Options { func TestRunView_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Contains(t, r.URL.Path, "/pages/12345") - assert.Equal(t, "GET", r.Method) - assert.Equal(t, "storage", r.URL.Query().Get("body-format"), "must request body-format=storage to get page content") + testutil.Contains(t, r.URL.Path, "/pages/12345") + testutil.Equal(t, "GET", r.Method) + testutil.Equal(t, "storage", r.URL.Query().Get("body-format")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -50,11 +49,11 @@ func TestRunView_Success(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) stdout := rootOpts.Stdout.(*bytes.Buffer) - assert.Contains(t, stdout.String(), "Hello", "should render storage content") - assert.Contains(t, stdout.String(), "World", "should render bold text") + testutil.Contains(t, stdout.String(), "Hello") + testutil.Contains(t, stdout.String(), "World") } func TestRunView_RawFormat(t *testing.T) { @@ -80,10 +79,10 @@ func TestRunView_RawFormat(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) stdout := rootOpts.Stdout.(*bytes.Buffer) - assert.Contains(t, stdout.String(), "

Raw HTML Content

", "should output raw storage HTML") + testutil.Contains(t, stdout.String(), "

Raw HTML Content

") } func TestRunView_JSONOutput(t *testing.T) { @@ -109,7 +108,7 @@ func TestRunView_JSONOutput(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunView_PageNotFound(t *testing.T) { @@ -128,8 +127,8 @@ func TestRunView_PageNotFound(t *testing.T) { } err := runView("99999", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to get page") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to get page") } func TestRunView_EmptyContent(t *testing.T) { @@ -153,7 +152,7 @@ func TestRunView_EmptyContent(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunView_InvalidOutputFormat(t *testing.T) { @@ -165,8 +164,8 @@ func TestRunView_InvalidOutputFormat(t *testing.T) { } err := runView("12345", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid output format") } func TestRunView_ShowMacros(t *testing.T) { @@ -192,7 +191,7 @@ func TestRunView_ShowMacros(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunView_ContentOnly(t *testing.T) { @@ -218,7 +217,7 @@ func TestRunView_ContentOnly(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Output should only contain markdown content, no Title:/ID:/Version: headers } @@ -246,7 +245,7 @@ func TestRunView_ContentOnly_Raw(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Output should only contain raw XHTML, no Title:/ID:/Version: headers } @@ -274,7 +273,7 @@ func TestRunView_ContentOnly_ShowMacros(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Output should contain markdown with [TOC] macro placeholder } @@ -288,8 +287,8 @@ func TestRunView_ContentOnly_JSON_Error(t *testing.T) { } err := runView("12345", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "--content-only is incompatible with --output json") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "--content-only is incompatible with --output json") } func TestRunView_ContentOnly_Web_Error(t *testing.T) { @@ -302,8 +301,8 @@ func TestRunView_ContentOnly_Web_Error(t *testing.T) { } err := runView("12345", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "--content-only is incompatible with --web") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "--content-only is incompatible with --web") } func TestRunView_ContentOnly_EmptyBody(t *testing.T) { @@ -328,7 +327,7 @@ func TestRunView_ContentOnly_EmptyBody(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Output should be "(No content)" without metadata headers } @@ -338,7 +337,7 @@ func TestRunView_WithSpaceKey(t *testing.T) { callCount++ if callCount == 1 { // First call: GetPage - assert.Contains(t, r.URL.Path, "/pages/12345") + testutil.Contains(t, r.URL.Path, "/pages/12345") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ "id": "12345", @@ -350,7 +349,7 @@ func TestRunView_WithSpaceKey(t *testing.T) { }`)) } else { // Second call: GetSpace - assert.Contains(t, r.URL.Path, "/spaces/98765") + testutil.Contains(t, r.URL.Path, "/spaces/98765") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ "id": "98765", @@ -371,8 +370,8 @@ func TestRunView_WithSpaceKey(t *testing.T) { } err := runView("12345", opts) - require.NoError(t, err) - assert.Equal(t, 2, callCount, "should call both GetPage and GetSpace") + testutil.RequireNoError(t, err) + testutil.Equal(t, 2, callCount) } func TestRunView_SpaceLookupFails_Graceful(t *testing.T) { @@ -408,7 +407,7 @@ func TestRunView_SpaceLookupFails_Graceful(t *testing.T) { // Should succeed even if space lookup fails err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestEnrichPageWithSpaceKey(t *testing.T) { @@ -420,45 +419,45 @@ func TestEnrichPageWithSpaceKey(t *testing.T) { enriched := enrichPageWithSpaceKey(page, "DEV") - assert.Equal(t, "12345", enriched.ID) - assert.Equal(t, "Test Page", enriched.Title) - assert.Equal(t, "DEV", enriched.SpaceKey) + testutil.Equal(t, "12345", enriched.ID) + testutil.Equal(t, "Test Page", enriched.Title) + testutil.Equal(t, "DEV", enriched.SpaceKey) } func TestTruncateContent(t *testing.T) { t.Run("short content is not truncated", func(t *testing.T) { opts := &viewOptions{} result := truncateContent("short", opts) - assert.Equal(t, "short", result) + testutil.Equal(t, "short", result) }) t.Run("long content is truncated by default", func(t *testing.T) { opts := &viewOptions{} long := strings.Repeat("x", maxViewChars+100) result := truncateContent(long, opts) - assert.Len(t, strings.SplitN(result, "\n\n... [truncated", 2)[0], maxViewChars) - assert.Contains(t, result, fmt.Sprintf("... [truncated at %d chars, use --full for complete text]", maxViewChars)) + testutil.Len(t, strings.SplitN(result, "\n\n... [truncated", 2)[0], maxViewChars) + testutil.Contains(t, result, fmt.Sprintf("... [truncated at %d chars, use --full for complete text]", maxViewChars)) }) t.Run("--full bypasses truncation", func(t *testing.T) { opts := &viewOptions{full: true} long := strings.Repeat("x", maxViewChars+100) result := truncateContent(long, opts) - assert.Equal(t, long, result) + testutil.Equal(t, long, result) }) t.Run("--content-only implies full", func(t *testing.T) { opts := &viewOptions{contentOnly: true} long := strings.Repeat("x", maxViewChars+100) result := truncateContent(long, opts) - assert.Equal(t, long, result) + testutil.Equal(t, long, result) }) t.Run("content at exact limit is not truncated", func(t *testing.T) { opts := &viewOptions{} exact := strings.Repeat("x", maxViewChars) result := truncateContent(exact, opts) - assert.Equal(t, exact, result) + testutil.Equal(t, exact, result) }) } @@ -508,13 +507,13 @@ func TestRunView_ADFPage_FallbackToAtlasDocFormat(t *testing.T) { opts := &viewOptions{Options: rootOpts} err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should make 3 calls: storage (empty), atlas_doc_format (has content), GetSpace - assert.Equal(t, 3, callCount, "should fallback to atlas_doc_format when storage is empty") + testutil.Equal(t, 3, callCount) stdout := rootOpts.Stdout.(*bytes.Buffer) - assert.Contains(t, stdout.String(), "Hello ADF", "should render ADF content as markdown") + testutil.Contains(t, stdout.String(), "Hello ADF") } func TestRunView_ADFPage_RawFormat(t *testing.T) { @@ -559,11 +558,11 @@ func TestRunView_ADFPage_RawFormat(t *testing.T) { opts := &viewOptions{Options: rootOpts, raw: true} err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) stdout := rootOpts.Stdout.(*bytes.Buffer) - assert.Contains(t, stdout.String(), "Raw ADF", "should output raw ADF JSON") - assert.Contains(t, stdout.String(), `"type"`, "should contain ADF JSON structure") + testutil.Contains(t, stdout.String(), "Raw ADF") + testutil.Contains(t, stdout.String(), `"type"`) } func TestRunView_StoragePage_NoFallback(t *testing.T) { @@ -597,13 +596,13 @@ func TestRunView_StoragePage_NoFallback(t *testing.T) { opts := &viewOptions{Options: rootOpts} err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should only make 2 calls: GetPage (storage has content) + GetSpace, no fallback - assert.Equal(t, 2, callCount, "should not fallback when storage has content") + testutil.Equal(t, 2, callCount) stdout := rootOpts.Stdout.(*bytes.Buffer) - assert.Contains(t, stdout.String(), "Has content", "should render storage content as markdown") + testutil.Contains(t, stdout.String(), "Has content") } func TestRunView_ADFPage_NullBody(t *testing.T) { @@ -643,8 +642,8 @@ func TestRunView_ADFPage_NullBody(t *testing.T) { opts := &viewOptions{Options: rootOpts} err := runView("12345", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) stdout := rootOpts.Stdout.(*bytes.Buffer) - assert.Contains(t, stdout.String(), "(No content)", "should display no content message") + testutil.Contains(t, stdout.String(), "(No content)") } diff --git a/tools/cfl/internal/cmd/search/search_test.go b/tools/cfl/internal/cmd/search/search_test.go index ba056c5..a01f212 100644 --- a/tools/cfl/internal/cmd/search/search_test.go +++ b/tools/cfl/internal/cmd/search/search_test.go @@ -7,8 +7,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -65,7 +64,7 @@ func TestRunSearch_Success(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_EmptyResults(t *testing.T) { @@ -88,7 +87,7 @@ func TestRunSearch_EmptyResults(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_JSONOutput(t *testing.T) { @@ -117,7 +116,7 @@ func TestRunSearch_JSONOutput(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_PlainOutput(t *testing.T) { @@ -146,7 +145,7 @@ func TestRunSearch_PlainOutput(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_InvalidOutputFormat(t *testing.T) { @@ -160,8 +159,8 @@ func TestRunSearch_InvalidOutputFormat(t *testing.T) { } err := runSearch(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid output format") } func TestRunSearch_InvalidType(t *testing.T) { @@ -175,9 +174,9 @@ func TestRunSearch_InvalidType(t *testing.T) { } err := runSearch(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid type") - assert.Contains(t, err.Error(), "invalid") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid type") + testutil.Contains(t, err.Error(), "invalid") } func TestRunSearch_ValidTypes(t *testing.T) { @@ -200,7 +199,7 @@ func TestRunSearch_ValidTypes(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) }) } } @@ -214,8 +213,8 @@ func TestRunSearch_NoQuery(t *testing.T) { } err := runSearch(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "search requires a query") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "search requires a query") } func TestRunSearch_NegativeLimit(t *testing.T) { @@ -228,8 +227,8 @@ func TestRunSearch_NegativeLimit(t *testing.T) { } err := runSearch(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid limit") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid limit") } func TestRunSearch_ZeroLimit(t *testing.T) { @@ -243,13 +242,13 @@ func TestRunSearch_ZeroLimit(t *testing.T) { // Zero limit should return empty without making API call err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_WithSpaceFilter(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cql := r.URL.Query().Get("cql") - assert.Contains(t, cql, `space = "DEV"`) + testutil.Contains(t, cql, `space = "DEV"`) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": [], "totalSize": 0}`)) @@ -268,13 +267,13 @@ func TestRunSearch_WithSpaceFilter(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_WithTypeFilter(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cql := r.URL.Query().Get("cql") - assert.Contains(t, cql, `type = "page"`) + testutil.Contains(t, cql, `type = "page"`) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": [], "totalSize": 0}`)) @@ -293,13 +292,13 @@ func TestRunSearch_WithTypeFilter(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_WithTitleFilter(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cql := r.URL.Query().Get("cql") - assert.Contains(t, cql, `title ~ "Getting Started"`) + testutil.Contains(t, cql, `title ~ "Getting Started"`) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": [], "totalSize": 0}`)) @@ -317,13 +316,13 @@ func TestRunSearch_WithTitleFilter(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_WithLabelFilter(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cql := r.URL.Query().Get("cql") - assert.Contains(t, cql, `label = "documentation"`) + testutil.Contains(t, cql, `label = "documentation"`) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": [], "totalSize": 0}`)) @@ -341,14 +340,14 @@ func TestRunSearch_WithLabelFilter(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_WithRawCQL(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cql := r.URL.Query().Get("cql") // Raw CQL should be used as-is - assert.Equal(t, `type=page AND lastModified > now("-7d")`, cql) + testutil.Equal(t, `type=page AND lastModified > now("-7d")`, cql) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": [], "totalSize": 0}`)) @@ -366,16 +365,16 @@ func TestRunSearch_WithRawCQL(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_CombinedFilters(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cql := r.URL.Query().Get("cql") - assert.Contains(t, cql, `text ~ "kubernetes"`) - assert.Contains(t, cql, `space = "DEV"`) - assert.Contains(t, cql, `type = "page"`) - assert.Contains(t, cql, `label = "infrastructure"`) + testutil.Contains(t, cql, `text ~ "kubernetes"`) + testutil.Contains(t, cql, `space = "DEV"`) + testutil.Contains(t, cql, `type = "page"`) + testutil.Contains(t, cql, `label = "infrastructure"`) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": [], "totalSize": 0}`)) @@ -396,7 +395,7 @@ func TestRunSearch_CombinedFilters(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_APIError(t *testing.T) { @@ -417,8 +416,8 @@ func TestRunSearch_APIError(t *testing.T) { } err := runSearch(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "search failed") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "search failed") } func TestRunSearch_HasMore(t *testing.T) { @@ -446,7 +445,7 @@ func TestRunSearch_HasMore(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_LongTitle(t *testing.T) { @@ -475,7 +474,7 @@ func TestRunSearch_LongTitle(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_SpaceOnlyFilter(t *testing.T) { @@ -494,13 +493,13 @@ func TestRunSearch_SpaceOnlyFilter(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunSearch_LimitParameter(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { limit := r.URL.Query().Get("limit") - assert.Equal(t, "50", limit) + testutil.Equal(t, "50", limit) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": [], "totalSize": 0}`)) @@ -518,7 +517,7 @@ func TestRunSearch_LimitParameter(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestExtractSpaceKey(t *testing.T) { @@ -567,7 +566,7 @@ func TestExtractSpaceKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := extractSpaceKey(tt.displayURL) - assert.Equal(t, tt.want, got) + testutil.Equal(t, tt.want, got) }) } } @@ -599,6 +598,6 @@ func TestRunSearch_DisplaysSpaceKey(t *testing.T) { } err := runSearch(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // The output should contain the space key "DEV" extracted from displayUrl } diff --git a/tools/cfl/internal/cmd/space/list_test.go b/tools/cfl/internal/cmd/space/list_test.go index e31edb7..f5f2a3c 100644 --- a/tools/cfl/internal/cmd/space/list_test.go +++ b/tools/cfl/internal/cmd/space/list_test.go @@ -6,8 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -24,8 +23,8 @@ func newTestRootOptions() *root.Options { func TestRunList_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "GET", r.Method) - assert.Contains(t, r.URL.Path, "/spaces") + testutil.Equal(t, "GET", r.Method) + testutil.Contains(t, r.URL.Path, "/spaces") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -59,7 +58,7 @@ func TestRunList_Success(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_EmptyResults(t *testing.T) { @@ -79,7 +78,7 @@ func TestRunList_EmptyResults(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_JSONOutput(t *testing.T) { @@ -104,7 +103,7 @@ func TestRunList_JSONOutput(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_InvalidOutputFormat(t *testing.T) { @@ -117,8 +116,8 @@ func TestRunList_InvalidOutputFormat(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid output format") } func TestRunList_NegativeLimit(t *testing.T) { @@ -130,8 +129,8 @@ func TestRunList_NegativeLimit(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid limit") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "invalid limit") } func TestRunList_ZeroLimit(t *testing.T) { @@ -144,7 +143,7 @@ func TestRunList_ZeroLimit(t *testing.T) { // Zero limit should return empty without making API call err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_ZeroLimitJSON(t *testing.T) { @@ -158,12 +157,12 @@ func TestRunList_ZeroLimitJSON(t *testing.T) { // Zero limit should return empty JSON array without making API call err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_WithTypeFilter(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "global", r.URL.Query().Get("type")) + testutil.Equal(t, "global", r.URL.Query().Get("type")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -185,12 +184,12 @@ func TestRunList_WithTypeFilter(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_WithLimitParameter(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "50", r.URL.Query().Get("limit")) + testutil.Equal(t, "50", r.URL.Query().Get("limit")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"results": []}`)) @@ -207,7 +206,7 @@ func TestRunList_WithLimitParameter(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_APIError(t *testing.T) { @@ -227,8 +226,8 @@ func TestRunList_APIError(t *testing.T) { } err := runList(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to list spaces") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to list spaces") } func TestRunList_HasMore(t *testing.T) { @@ -253,7 +252,7 @@ func TestRunList_HasMore(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_NullDescription(t *testing.T) { @@ -277,12 +276,12 @@ func TestRunList_NullDescription(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_WithCursor(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "abc123", r.URL.Query().Get("cursor")) + testutil.Equal(t, "abc123", r.URL.Query().Get("cursor")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -304,7 +303,7 @@ func TestRunList_WithCursor(t *testing.T) { } err := runList(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunList_DisplaysNextCursor(t *testing.T) { @@ -331,9 +330,9 @@ func TestRunList_DisplaysNextCursor(t *testing.T) { } err := runList(opts) - require.NoError(t, err) - assert.Contains(t, stderr.String(), "nextPageCursor123") - assert.Contains(t, stderr.String(), "--cursor") + testutil.RequireNoError(t, err) + testutil.Contains(t, stderr.String(), "nextPageCursor123") + testutil.Contains(t, stderr.String(), "--cursor") } func TestExtractCursor(t *testing.T) { @@ -367,7 +366,7 @@ func TestExtractCursor(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := extractCursor(tt.nextLink) - assert.Equal(t, tt.want, got) + testutil.Equal(t, tt.want, got) }) } } diff --git a/tools/cfl/internal/cmd/space/space_test.go b/tools/cfl/internal/cmd/space/space_test.go index 7877d60..848825f 100644 --- a/tools/cfl/internal/cmd/space/space_test.go +++ b/tools/cfl/internal/cmd/space/space_test.go @@ -8,8 +8,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/confluence-cli/api" "github.com/open-cli-collective/confluence-cli/internal/cmd/root" @@ -42,9 +41,9 @@ const v1SpaceUpdateResponse = `{ func TestRunView_Table(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "GET", r.Method) - assert.Contains(t, r.URL.Path, "/spaces") - assert.Equal(t, "TEST", r.URL.Query().Get("keys")) + testutil.Equal(t, "GET", r.Method) + testutil.Contains(t, r.URL.Path, "/spaces") + testutil.Equal(t, "TEST", r.URL.Query().Get("keys")) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(spaceListResponse)) @@ -64,12 +63,12 @@ func TestRunView_Table(t *testing.T) { opts := &viewOptions{Options: rootOpts} err := runView("TEST", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) output := stdout.String() - assert.Contains(t, output, "TEST") - assert.Contains(t, output, "Test Space") - assert.Contains(t, output, "global") - assert.Contains(t, output, "A test space") + testutil.Contains(t, output, "TEST") + testutil.Contains(t, output, "Test Space") + testutil.Contains(t, output, "global") + testutil.Contains(t, output, "A test space") } func TestRunView_JSON(t *testing.T) { @@ -92,11 +91,11 @@ func TestRunView_JSON(t *testing.T) { opts := &viewOptions{Options: rootOpts} err := runView("TEST", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) var result map[string]interface{} err = json.Unmarshal(stdout.Bytes(), &result) - require.NoError(t, err) - assert.Equal(t, "TEST", result["key"]) + testutil.RequireNoError(t, err) + testutil.Equal(t, "TEST", result["key"]) } func TestRunView_NotFound(t *testing.T) { @@ -113,23 +112,23 @@ func TestRunView_NotFound(t *testing.T) { opts := &viewOptions{Options: rootOpts} err := runView("NONEXISTENT", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "not found") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "not found") } // --- Create tests --- func TestRunCreate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "POST", r.Method) - assert.Equal(t, "/api/v2/spaces", r.URL.Path) + testutil.Equal(t, "POST", r.Method) + testutil.Equal(t, "/api/v2/spaces", r.URL.Path) var req api.CreateSpaceRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) - assert.Equal(t, "TEST", req.Key) - assert.Equal(t, "Test Space", req.Name) - assert.Equal(t, "global", req.Type) + testutil.RequireNoError(t, err) + testutil.Equal(t, "TEST", req.Key) + testutil.Equal(t, "Test Space", req.Name) + testutil.Equal(t, "global", req.Type) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -162,11 +161,11 @@ func TestRunCreate(t *testing.T) { err := runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) output := stdout.String() - assert.Contains(t, output, "Created space") - assert.Contains(t, output, "Test Space") - assert.Contains(t, output, "TEST") + testutil.Contains(t, output, "Created space") + testutil.Contains(t, output, "Test Space") + testutil.Contains(t, output, "TEST") } func TestRunCreate_JSON(t *testing.T) { @@ -201,20 +200,20 @@ func TestRunCreate_JSON(t *testing.T) { err := runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) var result map[string]interface{} err = json.Unmarshal(stdout.Bytes(), &result) - require.NoError(t, err) - assert.Equal(t, "TEST", result["key"]) + testutil.RequireNoError(t, err) + testutil.Equal(t, "TEST", result["key"]) } func TestRunCreate_WithDescription(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req api.CreateSpaceRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) - assert.NotNil(t, req.Description) - assert.Equal(t, "A test space", req.Description.Plain.Value) + testutil.RequireNoError(t, err) + testutil.NotNil(t, req.Description) + testutil.Equal(t, "A test space", req.Description.Plain.Value) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -240,21 +239,21 @@ func TestRunCreate_WithDescription(t *testing.T) { } err := runCreate(opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } // --- Update tests --- func TestRunUpdate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "PUT", r.Method) - assert.Equal(t, "/rest/api/space/TEST", r.URL.Path) + testutil.Equal(t, "PUT", r.Method) + testutil.Equal(t, "/rest/api/space/TEST", r.URL.Path) var req api.UpdateSpaceRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) - assert.Equal(t, "TEST", req.Key) - assert.Equal(t, "Updated Name", req.Name) + testutil.RequireNoError(t, err) + testutil.Equal(t, "TEST", req.Key) + testutil.Equal(t, "Updated Name", req.Name) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(v1SpaceUpdateResponse)) @@ -278,10 +277,10 @@ func TestRunUpdate(t *testing.T) { err := runUpdate("TEST", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) output := stdout.String() - assert.Contains(t, output, "Updated space") - assert.Contains(t, output, "Updated Name") + testutil.Contains(t, output, "Updated space") + testutil.Contains(t, output, "Updated Name") } func TestRunUpdate_JSON(t *testing.T) { @@ -308,11 +307,11 @@ func TestRunUpdate_JSON(t *testing.T) { err := runUpdate("TEST", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) var result map[string]interface{} err = json.Unmarshal(stdout.Bytes(), &result) - require.NoError(t, err) - assert.Equal(t, "TEST", result["key"]) + testutil.RequireNoError(t, err) + testutil.Equal(t, "TEST", result["key"]) } func TestRunUpdate_NoFlags(t *testing.T) { @@ -324,18 +323,18 @@ func TestRunUpdate_NoFlags(t *testing.T) { err := runUpdate("TEST", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "at least one of --name or --description is required") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "at least one of --name or --description is required") } func TestRunUpdate_WithDescription(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var req api.UpdateSpaceRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) - assert.NotNil(t, req.Description) - assert.Equal(t, "New description", req.Description.Plain.Value) - assert.Equal(t, "plain", req.Description.Plain.Representation) + testutil.RequireNoError(t, err) + testutil.NotNil(t, req.Description) + testutil.Equal(t, "New description", req.Description.Plain.Value) + testutil.Equal(t, "plain", req.Description.Plain.Representation) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(v1SpaceUpdateResponse)) @@ -352,7 +351,7 @@ func TestRunUpdate_WithDescription(t *testing.T) { } err := runUpdate("TEST", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } // --- Delete tests --- @@ -363,14 +362,14 @@ func TestRunDelete_Force(t *testing.T) { callCount++ if callCount == 1 { // GetSpaceByKey call - assert.Equal(t, "GET", r.Method) + testutil.Equal(t, "GET", r.Method) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(spaceListResponse)) return } // DeleteSpace call - assert.Equal(t, "DELETE", r.Method) - assert.Equal(t, "/rest/api/space/TEST", r.URL.Path) + testutil.Equal(t, "DELETE", r.Method) + testutil.Equal(t, "/rest/api/space/TEST", r.URL.Path) w.WriteHeader(http.StatusAccepted) })) defer server.Close() @@ -392,10 +391,10 @@ func TestRunDelete_Force(t *testing.T) { err := runDelete("TEST", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) output := stdout.String() - assert.Contains(t, output, "Deleted space") - assert.Contains(t, output, "Test Space") + testutil.Contains(t, output, "Deleted space") + testutil.Contains(t, output, "Test Space") } func TestRunDelete_Force_JSON(t *testing.T) { @@ -428,13 +427,13 @@ func TestRunDelete_Force_JSON(t *testing.T) { err := runDelete("TEST", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) var result map[string]string err = json.Unmarshal(stdout.Bytes(), &result) - require.NoError(t, err) - assert.Equal(t, "deleted", result["status"]) - assert.Equal(t, "TEST", result["space_key"]) - assert.Equal(t, "Test Space", result["name"]) + testutil.RequireNoError(t, err) + testutil.Equal(t, "deleted", result["status"]) + testutil.Equal(t, "TEST", result["space_key"]) + testutil.Equal(t, "Test Space", result["name"]) } func TestRunDelete_NoForce_Declined(t *testing.T) { @@ -456,7 +455,7 @@ func TestRunDelete_NoForce_Declined(t *testing.T) { err := runDelete("TEST", opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunDelete_NoForce_Accepted(t *testing.T) { @@ -484,8 +483,8 @@ func TestRunDelete_NoForce_Accepted(t *testing.T) { err := runDelete("TEST", opts) - require.NoError(t, err) - assert.Equal(t, 2, callCount) + testutil.RequireNoError(t, err) + testutil.Equal(t, 2, callCount) } func TestRunDelete_NotFound(t *testing.T) { @@ -506,6 +505,6 @@ func TestRunDelete_NotFound(t *testing.T) { err := runDelete("NONEXISTENT", opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "not found") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "not found") } diff --git a/tools/cfl/internal/config/config_test.go b/tools/cfl/internal/config/config_test.go index 76b4e76..1a66669 100644 --- a/tools/cfl/internal/config/config_test.go +++ b/tools/cfl/internal/config/config_test.go @@ -7,8 +7,7 @@ import ( "testing" sharedconfig "github.com/open-cli-collective/atlassian-go/config" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestConfig_Validate(t *testing.T) { @@ -70,10 +69,10 @@ func TestConfig_Validate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := tt.config.Validate() if tt.wantErr { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errMsg) + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), tt.errMsg) } else { - assert.NoError(t, err) + testutil.NoError(t, err) } }) } @@ -111,7 +110,7 @@ func TestConfig_NormalizeURL(t *testing.T) { t.Run(tt.name, func(t *testing.T) { cfg := Config{URL: tt.inputURL} cfg.NormalizeURL() - assert.Equal(t, tt.expected, cfg.URL) + testutil.Equal(t, tt.expected, cfg.URL) }) } } @@ -140,10 +139,10 @@ func TestConfig_LoadFromEnv(t *testing.T) { cfg := &Config{} cfg.LoadFromEnv() - assert.Equal(t, "https://env.atlassian.net", cfg.URL) - assert.Equal(t, "env@example.com", cfg.Email) - assert.Equal(t, "env-token", cfg.APIToken) - assert.Equal(t, "ENV", cfg.DefaultSpace) + testutil.Equal(t, "https://env.atlassian.net", cfg.URL) + testutil.Equal(t, "env@example.com", cfg.Email) + testutil.Equal(t, "env-token", cfg.APIToken) + testutil.Equal(t, "ENV", cfg.DefaultSpace) }) t.Run("env vars override existing values", func(t *testing.T) { @@ -159,9 +158,9 @@ func TestConfig_LoadFromEnv(t *testing.T) { cfg.LoadFromEnv() // URL should be overridden - assert.Equal(t, "https://override.atlassian.net", cfg.URL) + testutil.Equal(t, "https://override.atlassian.net", cfg.URL) // Email should remain (empty env var doesn't override) - assert.Equal(t, "original@example.com", cfg.Email) + testutil.Equal(t, "original@example.com", cfg.Email) }) } @@ -170,11 +169,11 @@ func TestDefaultConfigPath(t *testing.T) { // Should be under home directory home, err := os.UserHomeDir() - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.True(t, strings.HasPrefix(path, home)) - assert.Contains(t, path, "cfl") - assert.True(t, filepath.Ext(path) == ".yml" || filepath.Ext(path) == ".yaml") + testutil.True(t, strings.HasPrefix(path, home)) + testutil.Contains(t, path, "cfl") + testutil.True(t, filepath.Ext(path) == ".yml" || filepath.Ext(path) == ".yaml") } func TestConfig_Save_and_Load(t *testing.T) { @@ -192,22 +191,22 @@ func TestConfig_Save_and_Load(t *testing.T) { // Save err := original.Save(configPath) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Load loaded, err := Load(configPath) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, original.URL, loaded.URL) - assert.Equal(t, original.Email, loaded.Email) - assert.Equal(t, original.APIToken, loaded.APIToken) - assert.Equal(t, original.DefaultSpace, loaded.DefaultSpace) - assert.Equal(t, original.OutputFormat, loaded.OutputFormat) + testutil.Equal(t, original.URL, loaded.URL) + testutil.Equal(t, original.Email, loaded.Email) + testutil.Equal(t, original.APIToken, loaded.APIToken) + testutil.Equal(t, original.DefaultSpace, loaded.DefaultSpace) + testutil.Equal(t, original.OutputFormat, loaded.OutputFormat) } func TestLoad_FileNotFound(t *testing.T) { _, err := Load("/nonexistent/path/config.yml") - require.Error(t, err) + testutil.RequireError(t, err) } func TestConfig_LoadFromEnv_AtlassianFallback(t *testing.T) { @@ -232,9 +231,9 @@ func TestConfig_LoadFromEnv_AtlassianFallback(t *testing.T) { cfg := &Config{} cfg.LoadFromEnv() - assert.Equal(t, "https://shared.atlassian.net", cfg.URL) - assert.Equal(t, "shared@example.com", cfg.Email) - assert.Equal(t, "shared-token", cfg.APIToken) + testutil.Equal(t, "https://shared.atlassian.net", cfg.URL) + testutil.Equal(t, "shared@example.com", cfg.Email) + testutil.Equal(t, "shared-token", cfg.APIToken) }) t.Run("CFL_* takes precedence over ATLASSIAN_*", func(t *testing.T) { @@ -251,9 +250,9 @@ func TestConfig_LoadFromEnv_AtlassianFallback(t *testing.T) { cfg := &Config{} cfg.LoadFromEnv() - assert.Equal(t, "https://cfl.atlassian.net", cfg.URL) - assert.Equal(t, "cfl@example.com", cfg.Email) - assert.Equal(t, "cfl-token", cfg.APIToken) + testutil.Equal(t, "https://cfl.atlassian.net", cfg.URL) + testutil.Equal(t, "cfl@example.com", cfg.Email) + testutil.Equal(t, "cfl-token", cfg.APIToken) }) t.Run("mixed CFL_* and ATLASSIAN_*", func(t *testing.T) { @@ -268,9 +267,9 @@ func TestConfig_LoadFromEnv_AtlassianFallback(t *testing.T) { cfg := &Config{} cfg.LoadFromEnv() - assert.Equal(t, "https://cfl.atlassian.net", cfg.URL) - assert.Equal(t, "shared@example.com", cfg.Email) - assert.Equal(t, "shared-token", cfg.APIToken) + testutil.Equal(t, "https://cfl.atlassian.net", cfg.URL) + testutil.Equal(t, "shared@example.com", cfg.Email) + testutil.Equal(t, "shared-token", cfg.APIToken) }) } @@ -285,18 +284,18 @@ func TestGetEnvWithFallback(t *testing.T) { t.Run("returns primary when set", func(t *testing.T) { os.Setenv("TEST_PRIMARY", "primary-value") os.Setenv("TEST_FALLBACK", "fallback-value") - assert.Equal(t, "primary-value", sharedconfig.GetEnvWithFallback("TEST_PRIMARY", "TEST_FALLBACK")) + testutil.Equal(t, "primary-value", sharedconfig.GetEnvWithFallback("TEST_PRIMARY", "TEST_FALLBACK")) }) t.Run("returns fallback when primary empty", func(t *testing.T) { os.Unsetenv("TEST_PRIMARY") os.Setenv("TEST_FALLBACK", "fallback-value") - assert.Equal(t, "fallback-value", sharedconfig.GetEnvWithFallback("TEST_PRIMARY", "TEST_FALLBACK")) + testutil.Equal(t, "fallback-value", sharedconfig.GetEnvWithFallback("TEST_PRIMARY", "TEST_FALLBACK")) }) t.Run("returns empty when both empty", func(t *testing.T) { os.Unsetenv("TEST_PRIMARY") os.Unsetenv("TEST_FALLBACK") - assert.Equal(t, "", sharedconfig.GetEnvWithFallback("TEST_PRIMARY", "TEST_FALLBACK")) + testutil.Equal(t, "", sharedconfig.GetEnvWithFallback("TEST_PRIMARY", "TEST_FALLBACK")) }) } diff --git a/tools/cfl/pkg/md/codeprotect_test.go b/tools/cfl/pkg/md/codeprotect_test.go index ac0c99a..ef53acd 100644 --- a/tools/cfl/pkg/md/codeprotect_test.go +++ b/tools/cfl/pkg/md/codeprotect_test.go @@ -3,7 +3,7 @@ package md import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestProtectCodeRegions_FencedBlock(t *testing.T) { @@ -18,11 +18,11 @@ func TestProtectCodeRegions_FencedBlock(t *testing.T) { input: "before\n```\n[[My Page]]\n```\nafter", expectedRegion: 1, checkOutput: func(t *testing.T, output string, regions []codeRegion) { - assert.Contains(t, output, "before\n") - assert.Contains(t, output, "after") - assert.NotContains(t, output, "[[My Page]]") - assert.Contains(t, regions[0].content, "[[My Page]]") - assert.Contains(t, regions[0].content, "```") + testutil.Contains(t, output, "before\n") + testutil.Contains(t, output, "after") + testutil.NotContains(t, output, "[[My Page]]") + testutil.Contains(t, regions[0].content, "[[My Page]]") + testutil.Contains(t, regions[0].content, "```") }, }, { @@ -30,8 +30,8 @@ func TestProtectCodeRegions_FencedBlock(t *testing.T) { input: "before\n~~~\n[[My Page]]\n~~~\nafter", expectedRegion: 1, checkOutput: func(t *testing.T, output string, regions []codeRegion) { - assert.NotContains(t, output, "[[My Page]]") - assert.Contains(t, regions[0].content, "[[My Page]]") + testutil.NotContains(t, output, "[[My Page]]") + testutil.Contains(t, regions[0].content, "[[My Page]]") }, }, { @@ -39,9 +39,9 @@ func TestProtectCodeRegions_FencedBlock(t *testing.T) { input: "before\n```markdown\nUse [[Page Title]] syntax\n```\nafter", expectedRegion: 1, checkOutput: func(t *testing.T, output string, regions []codeRegion) { - assert.NotContains(t, output, "[[Page Title]]") - assert.Contains(t, regions[0].content, "[[Page Title]]") - assert.Contains(t, regions[0].content, "```markdown") + testutil.NotContains(t, output, "[[Page Title]]") + testutil.Contains(t, regions[0].content, "[[Page Title]]") + testutil.Contains(t, regions[0].content, "```markdown") }, }, { @@ -49,7 +49,7 @@ func TestProtectCodeRegions_FencedBlock(t *testing.T) { input: "See [[My Page]] for details", expectedRegion: 0, checkOutput: func(t *testing.T, output string, regions []codeRegion) { - assert.Equal(t, "See [[My Page]] for details", output) + testutil.Equal(t, "See [[My Page]] for details", output) }, }, { @@ -57,9 +57,9 @@ func TestProtectCodeRegions_FencedBlock(t *testing.T) { input: "```\n[[A]]\n```\ntext\n```\n[[B]]\n```", expectedRegion: 2, checkOutput: func(t *testing.T, output string, regions []codeRegion) { - assert.Contains(t, output, "text") - assert.NotContains(t, output, "[[A]]") - assert.NotContains(t, output, "[[B]]") + testutil.Contains(t, output, "text") + testutil.NotContains(t, output, "[[A]]") + testutil.NotContains(t, output, "[[B]]") }, }, } @@ -67,7 +67,7 @@ func TestProtectCodeRegions_FencedBlock(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { output, regions := protectCodeRegions([]byte(tt.input)) - assert.Equal(t, tt.expectedRegion, len(regions)) + testutil.Equal(t, tt.expectedRegion, len(regions)) tt.checkOutput(t, string(output), regions) }) } @@ -85,10 +85,10 @@ func TestProtectCodeRegions_InlineCode(t *testing.T) { input: "Use `[[My Page]]` for links", expectedRegion: 1, checkOutput: func(t *testing.T, output string, regions []codeRegion) { - assert.NotContains(t, output, "[[My Page]]") - assert.Contains(t, output, "Use ") - assert.Contains(t, output, " for links") - assert.Equal(t, "`[[My Page]]`", regions[0].content) + testutil.NotContains(t, output, "[[My Page]]") + testutil.Contains(t, output, "Use ") + testutil.Contains(t, output, " for links") + testutil.Equal(t, "`[[My Page]]`", regions[0].content) }, }, { @@ -96,8 +96,8 @@ func TestProtectCodeRegions_InlineCode(t *testing.T) { input: "Use ``[[My Page]]`` for links", expectedRegion: 1, checkOutput: func(t *testing.T, output string, regions []codeRegion) { - assert.NotContains(t, output, "[[My Page]]") - assert.Equal(t, "``[[My Page]]``", regions[0].content) + testutil.NotContains(t, output, "[[My Page]]") + testutil.Equal(t, "``[[My Page]]``", regions[0].content) }, }, { @@ -105,7 +105,7 @@ func TestProtectCodeRegions_InlineCode(t *testing.T) { input: "See [[My Page]] here", expectedRegion: 0, checkOutput: func(t *testing.T, output string, regions []codeRegion) { - assert.Equal(t, "See [[My Page]] here", output) + testutil.Equal(t, "See [[My Page]] here", output) }, }, { @@ -113,9 +113,9 @@ func TestProtectCodeRegions_InlineCode(t *testing.T) { input: "Use `[[syntax]]` to link to [[Real Page]]", expectedRegion: 1, checkOutput: func(t *testing.T, output string, regions []codeRegion) { - assert.Contains(t, output, "[[Real Page]]") - assert.NotContains(t, output, "`[[syntax]]`") - assert.Equal(t, "`[[syntax]]`", regions[0].content) + testutil.Contains(t, output, "[[Real Page]]") + testutil.NotContains(t, output, "`[[syntax]]`") + testutil.Equal(t, "`[[syntax]]`", regions[0].content) }, }, } @@ -123,7 +123,7 @@ func TestProtectCodeRegions_InlineCode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { output, regions := protectCodeRegions([]byte(tt.input)) - assert.Equal(t, tt.expectedRegion, len(regions)) + testutil.Equal(t, tt.expectedRegion, len(regions)) tt.checkOutput(t, string(output), regions) }) } @@ -136,13 +136,13 @@ func TestProtectCodeRegions_Mixed(t *testing.T) { outputStr := string(output) // Code regions should be protected - assert.Equal(t, 2, len(regions)) - assert.NotContains(t, outputStr, "[[Page B]]") - assert.NotContains(t, outputStr, "[[Page C]]") + testutil.Equal(t, 2, len(regions)) + testutil.NotContains(t, outputStr, "[[Page B]]") + testutil.NotContains(t, outputStr, "[[Page C]]") // Non-code wiki links should remain - assert.Contains(t, outputStr, "[[Page A]]") - assert.Contains(t, outputStr, "[[Page D]]") + testutil.Contains(t, outputStr, "[[Page A]]") + testutil.Contains(t, outputStr, "[[Page D]]") } func TestRestoreCodeRegions(t *testing.T) { @@ -153,26 +153,26 @@ func TestRestoreCodeRegions(t *testing.T) { // Simulate wiki-link replacement on the non-code parts protectedStr := string(protected) - assert.Contains(t, protectedStr, "[[Link]]") + testutil.Contains(t, protectedStr, "[[Link]]") // Restore restored := restoreCodeRegions(protected, regions) - assert.Equal(t, input, string(restored)) + testutil.Equal(t, input, string(restored)) } func TestProtectCodeRegions_UnclosedFence(t *testing.T) { // Unclosed fence should protect to end of input input := "before\n```\n[[My Page]]\nno closing fence" output, regions := protectCodeRegions([]byte(input)) - assert.Equal(t, 1, len(regions)) - assert.Contains(t, string(output), "before\n") - assert.NotContains(t, string(output), "[[My Page]]") + testutil.Equal(t, 1, len(regions)) + testutil.Contains(t, string(output), "before\n") + testutil.NotContains(t, string(output), "[[My Page]]") } func TestProtectCodeRegions_UnmatchedBacktick(t *testing.T) { // Unmatched backtick should not swallow content input := "text `unclosed [[My Page]]" output, regions := protectCodeRegions([]byte(input)) - assert.Equal(t, 0, len(regions)) - assert.Equal(t, input, string(output)) + testutil.Equal(t, 0, len(regions)) + testutil.Equal(t, input, string(output)) } diff --git a/tools/cfl/pkg/md/converter_test.go b/tools/cfl/pkg/md/converter_test.go index 26a1800..28c635a 100644 --- a/tools/cfl/pkg/md/converter_test.go +++ b/tools/cfl/pkg/md/converter_test.go @@ -4,8 +4,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestToConfluenceStorage(t *testing.T) { @@ -114,8 +113,8 @@ func TestToConfluenceStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToConfluenceStorage([]byte(tt.input)) - require.NoError(t, err) - assert.Equal(t, tt.expected, result) + testutil.RequireNoError(t, err) + testutil.Equal(t, tt.expected, result) }) } } @@ -143,17 +142,17 @@ For more info, see [the docs](https://example.com). ` result, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify key elements are present - assert.Contains(t, result, "

Project README

") - assert.Contains(t, result, "introduction") - assert.Contains(t, result, "

Features

") - assert.Contains(t, result, "
  • Feature one
  • ") - assert.Contains(t, result, "

    Code Example

    ") - assert.Contains(t, result, "language-go") - assert.Contains(t, result, "fmt.Println") - assert.Contains(t, result, `the docs`) + testutil.Contains(t, result, "

    Project README

    ") + testutil.Contains(t, result, "introduction") + testutil.Contains(t, result, "

    Features

    ") + testutil.Contains(t, result, "
  • Feature one
  • ") + testutil.Contains(t, result, "

    Code Example

    ") + testutil.Contains(t, result, "language-go") + testutil.Contains(t, result, "fmt.Println") + testutil.Contains(t, result, `the docs`) } func TestToConfluenceStorage_TOCMacro(t *testing.T) { @@ -220,9 +219,9 @@ func TestToConfluenceStorage_TOCMacro(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToConfluenceStorage([]byte(tt.input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) for _, expected := range tt.contains { - assert.Contains(t, result, expected, "should contain: %s", expected) + testutil.Contains(t, result, expected) } }) } @@ -240,17 +239,17 @@ Some content here. More content. ` result, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify TOC macro is present - assert.Contains(t, result, ``) - assert.Contains(t, result, `3`) - assert.Contains(t, result, ``) + testutil.Contains(t, result, ``) + testutil.Contains(t, result, `3`) + testutil.Contains(t, result, ``) // Verify other content is preserved - assert.Contains(t, result, "

    Heading 1

    ") - assert.Contains(t, result, "Some content here.") - assert.Contains(t, result, "

    Heading 2

    ") + testutil.Contains(t, result, "

    Heading 1

    ") + testutil.Contains(t, result, "Some content here.") + testutil.Contains(t, result, "

    Heading 2

    ") } func TestToConfluenceStorage_TOCRoundtrip(t *testing.T) { @@ -267,21 +266,21 @@ func TestToConfluenceStorage_TOCRoundtrip(t *testing.T) { // Convert to markdown with ShowMacros opts := ConvertOptions{ShowMacros: true} markdown, err := FromConfluenceStorageWithOptions(originalXHTML, opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify markdown has TOC placeholder with params - assert.Contains(t, markdown, "[TOC") - assert.Contains(t, markdown, "maxLevel=3") - assert.Contains(t, markdown, "minLevel=1") + testutil.Contains(t, markdown, "[TOC") + testutil.Contains(t, markdown, "maxLevel=3") + testutil.Contains(t, markdown, "minLevel=1") // Convert back to storage format resultXHTML, err := ToConfluenceStorage([]byte(markdown)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify TOC macro is restored - assert.Contains(t, resultXHTML, ``) - assert.Contains(t, resultXHTML, `3`) - assert.Contains(t, resultXHTML, `1`) + testutil.Contains(t, resultXHTML, ``) + testutil.Contains(t, resultXHTML, `3`) + testutil.Contains(t, resultXHTML, `1`) } func TestParseKeyValueParams(t *testing.T) { @@ -325,7 +324,7 @@ func TestParseKeyValueParams(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := parseKeyValueParams(tt.input) - assert.Equal(t, tt.expected, result) + testutil.Equal(t, tt.expected, result) }) } } @@ -430,9 +429,9 @@ func TestToConfluenceStorage_PanelMacros(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToConfluenceStorage([]byte(tt.input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) for _, expected := range tt.contains { - assert.Contains(t, result, expected, "should contain: %s", expected) + testutil.Contains(t, result, expected) } }) } @@ -450,16 +449,16 @@ This is a warning. More text after. ` result, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify all parts are present - assert.Contains(t, result, "

    Heading

    ") - assert.Contains(t, result, "Some intro text.") - assert.Contains(t, result, ``) - assert.Contains(t, result, `Important`) - assert.Contains(t, result, "This is a warning.") - assert.Contains(t, result, "") - assert.Contains(t, result, "More text after.") + testutil.Contains(t, result, "

    Heading

    ") + testutil.Contains(t, result, "Some intro text.") + testutil.Contains(t, result, ``) + testutil.Contains(t, result, `Important`) + testutil.Contains(t, result, "This is a warning.") + testutil.Contains(t, result, "") + testutil.Contains(t, result, "More text after.") } func TestToConfluenceStorage_PanelRoundtrip(t *testing.T) { @@ -475,23 +474,23 @@ func TestToConfluenceStorage_PanelRoundtrip(t *testing.T) { // Convert to markdown with ShowMacros opts := ConvertOptions{ShowMacros: true} markdown, err := FromConfluenceStorageWithOptions(originalXHTML, opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify markdown has panel placeholder (brackets may be escaped by markdown converter) - assert.Contains(t, markdown, "INFO") - assert.Contains(t, markdown, "title=Important") - assert.Contains(t, markdown, "Panel content") + testutil.Contains(t, markdown, "INFO") + testutil.Contains(t, markdown, "title=Important") + testutil.Contains(t, markdown, "Panel content") // Convert back to storage format resultXHTML, err := ToConfluenceStorage([]byte(markdown)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify panel macro is restored - assert.Contains(t, resultXHTML, ``) - assert.Contains(t, resultXHTML, `Important`) - assert.Contains(t, resultXHTML, ``) - assert.Contains(t, resultXHTML, `Panel content`) - assert.Contains(t, resultXHTML, ``) + testutil.Contains(t, resultXHTML, ``) + testutil.Contains(t, resultXHTML, `Important`) + testutil.Contains(t, resultXHTML, ``) + testutil.Contains(t, resultXHTML, `Panel content`) + testutil.Contains(t, resultXHTML, ``) } func TestToConfluenceStorage_NestedMacros(t *testing.T) { @@ -501,15 +500,15 @@ Check out the table of contents: [TOC] [/INFO]` result, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // The result should have both the panel macro and TOC macro - assert.Contains(t, result, ` infoEnd := strings.LastIndex(result, ``) - assert.True(t, infoStart < richTextStart, "INFO should start before rich-text-body") - assert.True(t, richTextStart < tocStart, "rich-text-body should start before TOC") - assert.True(t, tocStart < richTextEnd, "TOC should be before rich-text-body end") - assert.True(t, richTextEnd < infoEnd, "rich-text-body should end before INFO") + testutil.True(t, infoStart < richTextStart, "INFO should start before rich-text-body") + testutil.True(t, richTextStart < tocStart, "rich-text-body should start before TOC") + testutil.True(t, tocStart < richTextEnd, "TOC should be before rich-text-body end") + testutil.True(t, richTextEnd < infoEnd, "rich-text-body should end before INFO") } // TestToConfluenceStorage_MacrosInCodeBlock verifies that bracket macros inside fenced @@ -700,12 +699,12 @@ func TestToConfluenceStorage_MacrosInCodeBlock(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToConfluenceStorage([]byte(tt.input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) for _, s := range tt.contains { - assert.Contains(t, result, s, "should contain: %s", s) + testutil.Contains(t, result, s) } for _, s := range tt.notContains { - assert.NotContains(t, result, s, "should not contain: %s", s) + testutil.NotContains(t, result, s) } }) } @@ -726,10 +725,10 @@ More outer // Run 100 times to catch non-deterministic behavior for i := 0; i < 100; i++ { result, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err, "iteration %d", i) - assert.Contains(t, result, `ac:name="toc"`, "iteration %d: TOC macro missing", i) - assert.Contains(t, result, `ac:name="warning"`, "iteration %d: WARNING macro missing", i) - assert.Contains(t, result, `ac:name="info"`, "iteration %d: INFO macro missing", i) - assert.NotContains(t, result, "CFMACRO", "iteration %d: unresolved placeholder", i) + testutil.RequireNoError(t, err) + testutil.Contains(t, result, `ac:name="toc"`) + testutil.Contains(t, result, `ac:name="warning"`) + testutil.Contains(t, result, `ac:name="info"`) + testutil.NotContains(t, result, "CFMACRO") } } diff --git a/tools/cfl/pkg/md/from_adf_test.go b/tools/cfl/pkg/md/from_adf_test.go index 50bd3d9..f60de79 100644 --- a/tools/cfl/pkg/md/from_adf_test.go +++ b/tools/cfl/pkg/md/from_adf_test.go @@ -4,34 +4,33 @@ import ( "encoding/json" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestFromADF_EmptyInput(t *testing.T) { result, err := FromADF("") - require.NoError(t, err) - assert.Equal(t, "", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "", result) } func TestFromADF_EmptyDocument(t *testing.T) { input := `{"type":"doc","version":1,"content":[]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "", result) } func TestFromADF_InvalidJSON(t *testing.T) { _, err := FromADF("{invalid") - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to parse ADF JSON") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "failed to parse ADF JSON") } func TestFromADF_Paragraph(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"Hello world"}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "Hello world\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "Hello world\n", result) } func TestFromADF_Headings(t *testing.T) { @@ -53,8 +52,8 @@ func TestFromADF_Headings(t *testing.T) { t.Run(tt.name, func(t *testing.T) { input := adfDoc(adfHeading(tt.level, tt.text)) result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, tt.expected, result) + testutil.RequireNoError(t, err) + testutil.Equal(t, tt.expected, result) }) } } @@ -62,36 +61,36 @@ func TestFromADF_Headings(t *testing.T) { func TestFromADF_Bold(t *testing.T) { input := adfDoc(adfPara(adfMarkedText("bold text", "strong"))) result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "**bold text**\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "**bold text**\n", result) } func TestFromADF_Italic(t *testing.T) { input := adfDoc(adfPara(adfMarkedText("italic text", "em"))) result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "*italic text*\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "*italic text*\n", result) } func TestFromADF_InlineCode(t *testing.T) { input := adfDoc(adfPara(adfMarkedText("fmt.Println()", "code"))) result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "`fmt.Println()`\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "`fmt.Println()`\n", result) } func TestFromADF_Strikethrough(t *testing.T) { input := adfDoc(adfPara(adfMarkedText("deleted", "strike"))) result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "~~deleted~~\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "~~deleted~~\n", result) } func TestFromADF_Link(t *testing.T) { input := adfDoc(adfPara(`{"type":"text","text":"click here","marks":[{"type":"link","attrs":{"href":"https://example.com"}}]}`)) result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "[click here](https://example.com)\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "[click here](https://example.com)\n", result) } func TestFromADF_MixedInline(t *testing.T) { @@ -102,186 +101,186 @@ func TestFromADF_MixedInline(t *testing.T) { adfMarkedText("code", "code"), )) result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "Hello **world** and `code`\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "Hello **world** and `code`\n", result) } func TestFromADF_BulletList(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item one"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item two"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item three"}]}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "- Item one\n- Item two\n- Item three\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "- Item one\n- Item two\n- Item three\n", result) } func TestFromADF_OrderedList(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"orderedList","attrs":{"order":1},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"First"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Second"}]}]},{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Third"}]}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "1. First\n2. Second\n3. Third\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "1. First\n2. Second\n3. Third\n", result) } func TestFromADF_NestedList(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Outer"}]},{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Inner"}]}]}]}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "- Outer") - assert.Contains(t, result, " - Inner") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "- Outer") + testutil.Contains(t, result, " - Inner") } func TestFromADF_CodeBlock_NoLanguage(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"codeBlock","content":[{"type":"text","text":"hello world"}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "```\nhello world\n```\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "```\nhello world\n```\n", result) } func TestFromADF_CodeBlock_WithLanguage(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"codeBlock","attrs":{"language":"go"},"content":[{"type":"text","text":"fmt.Println(\"hello\")"}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "```go\nfmt.Println(\"hello\")\n```\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "```go\nfmt.Println(\"hello\")\n```\n", result) } func TestFromADF_Blockquote(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"blockquote","content":[{"type":"paragraph","content":[{"type":"text","text":"Quoted text"}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "> Quoted text\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "> Quoted text\n", result) } func TestFromADF_HorizontalRule(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"Above"}]},{"type":"rule"},{"type":"paragraph","content":[{"type":"text","text":"Below"}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "Above") - assert.Contains(t, result, "---") - assert.Contains(t, result, "Below") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "Above") + testutil.Contains(t, result, "---") + testutil.Contains(t, result, "Below") } func TestFromADF_Table(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"table","attrs":{"layout":"default"},"content":[{"type":"tableRow","content":[{"type":"tableHeader","content":[{"type":"paragraph","content":[{"type":"text","text":"Name"}]}]},{"type":"tableHeader","content":[{"type":"paragraph","content":[{"type":"text","text":"Value"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","content":[{"type":"paragraph","content":[{"type":"text","text":"A"}]}]},{"type":"tableCell","content":[{"type":"paragraph","content":[{"type":"text","text":"1"}]}]}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "| Name") - assert.Contains(t, result, "| A") - assert.Contains(t, result, "---") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "| Name") + testutil.Contains(t, result, "| A") + testutil.Contains(t, result, "---") } func TestFromADF_HardBreak(t *testing.T) { input := adfDoc(adfPara(`{"type":"text","text":"Line one"}`, `{"type":"hardBreak"}`, `{"type":"text","text":"Line two"}`)) result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "Line one \nLine two\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "Line one \nLine two\n", result) } func TestFromADF_Extension_TOC(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"extension","attrs":{"extensionType":"com.atlassian.confluence.macro.core","extensionKey":"toc","layout":"default"}}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "[TOC]\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "[TOC]\n", result) } func TestFromADF_Extension_TOC_WithParams(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"extension","attrs":{"extensionType":"com.atlassian.confluence.macro.core","extensionKey":"toc","parameters":{"maxLevel":{"value":"3"}},"layout":"default"}}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Equal(t, "[TOC maxLevel=3]\n", result) + testutil.RequireNoError(t, err) + testutil.Equal(t, "[TOC maxLevel=3]\n", result) } func TestFromADF_Panel_Info(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"panel","attrs":{"panelType":"info"},"content":[{"type":"paragraph","content":[{"type":"text","text":"Important info"}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "[INFO]") - assert.Contains(t, result, "Important info") - assert.Contains(t, result, "[/INFO]") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "[INFO]") + testutil.Contains(t, result, "Important info") + testutil.Contains(t, result, "[/INFO]") } func TestFromADF_Panel_Warning(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"panel","attrs":{"panelType":"warning"},"content":[{"type":"paragraph","content":[{"type":"text","text":"Be careful"}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "[WARNING]") - assert.Contains(t, result, "Be careful") - assert.Contains(t, result, "[/WARNING]") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "[WARNING]") + testutil.Contains(t, result, "Be careful") + testutil.Contains(t, result, "[/WARNING]") } func TestFromADF_BodiedExtension_Expand(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"bodiedExtension","attrs":{"extensionType":"com.atlassian.confluence.macro.core","extensionKey":"expand","parameters":{"title":{"value":"Click me"}},"layout":"default"},"content":[{"type":"paragraph","content":[{"type":"text","text":"Hidden content"}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "[EXPAND title=Click me]") - assert.Contains(t, result, "Hidden content") - assert.Contains(t, result, "[/EXPAND]") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "[EXPAND title=Click me]") + testutil.Contains(t, result, "Hidden content") + testutil.Contains(t, result, "[/EXPAND]") } func TestFromADF_EmptyParagraph(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"paragraph"}]}` result, err := FromADF(input) - require.NoError(t, err) + testutil.RequireNoError(t, err) // An empty paragraph produces just a newline, which gets trimmed to empty. - assert.Equal(t, "", result) + testutil.Equal(t, "", result) } func TestFromADF_MultipleBlocks(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Title"}]},{"type":"paragraph","content":[{"type":"text","text":"Some text"}]},{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item"}]}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "# Title") - assert.Contains(t, result, "Some text") - assert.Contains(t, result, "- Item") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "# Title") + testutil.Contains(t, result, "Some text") + testutil.Contains(t, result, "- Item") } func TestFromADF_UnknownNodeFallback(t *testing.T) { input := `{"type":"doc","version":1,"content":[{"type":"customWidget","text":"fallback text"}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "fallback text") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "fallback text") } func TestFromADF_InlineCard(t *testing.T) { input := adfDoc(adfPara(`{"type":"inlineCard","attrs":{"url":"https://example.com/page"}}`)) result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "https://example.com/page") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "https://example.com/page") } func TestFromADF_ListItem_ContinuationParagraph(t *testing.T) { // List item with two paragraphs: first gets bullet prefix, second gets indent only. input := `{"type":"doc","version":1,"content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"First para"}]},{"type":"paragraph","content":[{"type":"text","text":"Second para"}]}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "- First para") - assert.Contains(t, result, " Second para") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "- First para") + testutil.Contains(t, result, " Second para") // Second paragraph should NOT have a bullet prefix. - assert.NotContains(t, result, "- Second para") + testutil.NotContains(t, result, "- Second para") } func TestFromADF_ListItem_NestedOrderedList(t *testing.T) { // Bullet list item containing a nested ordered list. input := `{"type":"doc","version":1,"content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Outer"}]},{"type":"orderedList","attrs":{"order":1},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Inner"}]}]}]}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "- Outer") - assert.Contains(t, result, " 1. Inner") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "- Outer") + testutil.Contains(t, result, " 1. Inner") } func TestFromADF_ListItem_WithCodeBlock(t *testing.T) { // List item containing a paragraph followed by a code block. input := `{"type":"doc","version":1,"content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Item with code"}]},{"type":"codeBlock","attrs":{"language":"go"},"content":[{"type":"text","text":"fmt.Println()"}]}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "- Item with code") - assert.Contains(t, result, "```go") - assert.Contains(t, result, "fmt.Println()") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "- Item with code") + testutil.Contains(t, result, "```go") + testutil.Contains(t, result, "fmt.Println()") } func TestFromADF_ListItem_DefaultChild(t *testing.T) { // List item containing a blockquote (falls through to the default case). input := `{"type":"doc","version":1,"content":[{"type":"bulletList","content":[{"type":"listItem","content":[{"type":"blockquote","content":[{"type":"paragraph","content":[{"type":"text","text":"Quoted"}]}]}]}]}]}` result, err := FromADF(input) - require.NoError(t, err) - assert.Contains(t, result, "- > Quoted") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "- > Quoted") } // Test helpers for building ADF JSON strings. diff --git a/tools/cfl/pkg/md/from_html_test.go b/tools/cfl/pkg/md/from_html_test.go index 2c603a0..99a1cf6 100644 --- a/tools/cfl/pkg/md/from_html_test.go +++ b/tools/cfl/pkg/md/from_html_test.go @@ -4,8 +4,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestFromConfluenceStorage(t *testing.T) { @@ -94,8 +93,8 @@ func TestFromConfluenceStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := FromConfluenceStorage(tt.input) - require.NoError(t, err) - assert.Equal(t, tt.expected, result) + testutil.RequireNoError(t, err) + testutil.Equal(t, tt.expected, result) }) } } @@ -160,9 +159,9 @@ func TestFromConfluenceStorage_ConfluenceCodeMacro(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := FromConfluenceStorage(tt.input) - require.NoError(t, err) + testutil.RequireNoError(t, err) for _, expected := range tt.contains { - assert.Contains(t, result, expected, "should contain: %s", expected) + testutil.Contains(t, result, expected) } }) } @@ -216,9 +215,9 @@ func TestFromConfluenceStorage_Tables(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := FromConfluenceStorage(tt.input) - require.NoError(t, err) + testutil.RequireNoError(t, err) for _, expected := range tt.contains { - assert.Contains(t, result, expected, "should contain: %s", expected) + testutil.Contains(t, result, expected) } }) } @@ -233,11 +232,11 @@ func TestFromConfluenceStorage_NonCodeMacrosStripped(t *testing.T) {

    After

    ` result, err := FromConfluenceStorage(input) - require.NoError(t, err) - assert.Contains(t, result, "Before") - assert.Contains(t, result, "After") - assert.NotContains(t, result, "toc") - assert.NotContains(t, result, "maxLevel") + testutil.RequireNoError(t, err) + testutil.Contains(t, result, "Before") + testutil.Contains(t, result, "After") + testutil.NotContains(t, result, "toc") + testutil.NotContains(t, result, "maxLevel") } func TestFromConfluenceStorage_TOCWithShowMacros(t *testing.T) { @@ -288,8 +287,8 @@ func TestFromConfluenceStorage_TOCWithShowMacros(t *testing.T) { t.Run(tt.name, func(t *testing.T) { opts := ConvertOptions{ShowMacros: true} result, err := FromConfluenceStorageWithOptions(tt.input, opts) - require.NoError(t, err) - assert.Contains(t, result, tt.expected) + testutil.RequireNoError(t, err) + testutil.Contains(t, result, tt.expected) }) } } @@ -367,9 +366,9 @@ func TestFromConfluenceStorage_PanelMacrosWithShowMacros(t *testing.T) { t.Run(tt.name, func(t *testing.T) { opts := ConvertOptions{ShowMacros: true} result, err := FromConfluenceStorageWithOptions(tt.input, opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) for _, expected := range tt.contains { - assert.Contains(t, result, expected, "should contain: %s", expected) + testutil.Contains(t, result, expected) } }) } @@ -391,17 +390,17 @@ func TestFromConfluenceStorage_ComplexDocument(t *testing.T) {

    For more info, see the docs.

    ` result, err := FromConfluenceStorage(input) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify key elements are present - assert.Contains(t, result, "# Project README") - assert.Contains(t, result, "**introduction**") - assert.Contains(t, result, "## Features") - assert.Contains(t, result, "- Feature one") - assert.Contains(t, result, "## Code Example") - assert.Contains(t, result, "```") - assert.Contains(t, result, "fmt.Println") - assert.Contains(t, result, "[the docs](https://example.com)") + testutil.Contains(t, result, "# Project README") + testutil.Contains(t, result, "**introduction**") + testutil.Contains(t, result, "## Features") + testutil.Contains(t, result, "- Feature one") + testutil.Contains(t, result, "## Code Example") + testutil.Contains(t, result, "```") + testutil.Contains(t, result, "fmt.Println") + testutil.Contains(t, result, "[the docs](https://example.com)") } func TestFromConfluenceStorage_NestedMacros(t *testing.T) { @@ -418,16 +417,16 @@ func TestFromConfluenceStorage_NestedMacros(t *testing.T) { opts := ConvertOptions{ShowMacros: true} result, err := FromConfluenceStorageWithOptions(input, opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should have both INFO panel and nested TOC - assert.Contains(t, result, "[INFO]") - assert.Contains(t, result, "[/INFO]") - assert.Contains(t, result, "[TOC maxLevel=2]") - assert.Contains(t, result, "# Title") + testutil.Contains(t, result, "[INFO]") + testutil.Contains(t, result, "[/INFO]") + testutil.Contains(t, result, "[TOC maxLevel=2]") + testutil.Contains(t, result, "# Title") // INFO should not have TOC's parameters - assert.NotContains(t, result, "[INFO maxLevel=2]") + testutil.NotContains(t, result, "[INFO maxLevel=2]") } // TestXHTMLToMD_NestedMacroPositionPreserved verifies that when converting XHTML back to @@ -492,11 +491,11 @@ func TestXHTMLToMD_NestedMacroPositionPreserved(t *testing.T) { t.Run(tt.name, func(t *testing.T) { opts := ConvertOptions{ShowMacros: true} result, err := FromConfluenceStorageWithOptions(tt.input, opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify all expected strings are present for _, expected := range tt.verifyOrder { - assert.Contains(t, result, expected, "should contain: %s", expected) + testutil.Contains(t, result, expected) } // Verify order: each string should come after the previous one @@ -514,9 +513,9 @@ func TestXHTMLToMD_NestedMacroPositionPreserved(t *testing.T) { } // No placeholders should remain - assert.NotContains(t, result, "CFXMLCHILD", "XML child placeholders should be replaced") - assert.NotContains(t, result, "CFMACROOPEN", "Macro placeholders should be replaced") - assert.NotContains(t, result, "CFMACROCLOSE", "Macro placeholders should be replaced") + testutil.NotContains(t, result, "CFXMLCHILD") + testutil.NotContains(t, result, "CFMACROOPEN") + testutil.NotContains(t, result, "CFMACROCLOSE") }) } } @@ -539,18 +538,18 @@ func TestXHTMLToMD_NestedMacroOrderPreserved(t *testing.T) { opts := ConvertOptions{ShowMacros: true} result, err := FromConfluenceStorageWithOptions(input, opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) beforeIdx := strings.Index(result, "Before") tocIdx := strings.Index(result, "[TOC]") afterIdx := strings.Index(result, "After") - assert.True(t, beforeIdx >= 0, "Before should be found") - assert.True(t, tocIdx >= 0, "[TOC] should be found") - assert.True(t, afterIdx >= 0, "After should be found") + testutil.True(t, beforeIdx >= 0, "Before should be found") + testutil.True(t, tocIdx >= 0, "[TOC] should be found") + testutil.True(t, afterIdx >= 0, "After should be found") - assert.True(t, beforeIdx < tocIdx, "Before should come before [TOC]") - assert.True(t, tocIdx < afterIdx, "[TOC] should come before After") + testutil.True(t, beforeIdx < tocIdx, "Before should come before [TOC]") + testutil.True(t, tocIdx < afterIdx, "[TOC] should come before After") } // TestFromConfluenceStorage_NestedMacroInParagraph tests the exact bug scenario from issue #56. @@ -567,25 +566,25 @@ func TestFromConfluenceStorage_NestedMacroInParagraph(t *testing.T) { opts := ConvertOptions{ShowMacros: true} result, err := FromConfluenceStorageWithOptions(input, opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should contain both INFO and TOC macros without "unclosed macro" warning - assert.Contains(t, result, "[INFO]", "should contain [INFO] macro") - assert.Contains(t, result, "[TOC]", "should contain [TOC] macro") - assert.Contains(t, result, "[/INFO]", "should contain [/INFO] close tag") - assert.Contains(t, result, "# Header 1", "should contain header") + testutil.Contains(t, result, "[INFO]") + testutil.Contains(t, result, "[TOC]") + testutil.Contains(t, result, "[/INFO]") + testutil.Contains(t, result, "# Header 1") // TOC should be inside INFO (between [INFO] and [/INFO]) infoStart := strings.Index(result, "[INFO]") tocPos := strings.Index(result, "[TOC]") infoEnd := strings.Index(result, "[/INFO]") - assert.True(t, infoStart >= 0, "[INFO] should be found") - assert.True(t, tocPos >= 0, "[TOC] should be found") - assert.True(t, infoEnd >= 0, "[/INFO] should be found") + testutil.True(t, infoStart >= 0, "[INFO] should be found") + testutil.True(t, tocPos >= 0, "[TOC] should be found") + testutil.True(t, infoEnd >= 0, "[/INFO] should be found") - assert.True(t, infoStart < tocPos, "[INFO] should come before [TOC]") - assert.True(t, tocPos < infoEnd, "[TOC] should come before [/INFO]") + testutil.True(t, infoStart < tocPos, "[INFO] should come before [TOC]") + testutil.True(t, tocPos < infoEnd, "[TOC] should come before [/INFO]") } // TestFromConfluenceStorage_MultipleSelfClosingNestedMacros tests multiple self-closing @@ -603,15 +602,15 @@ func TestFromConfluenceStorage_MultipleSelfClosingNestedMacros(t *testing.T) { opts := ConvertOptions{ShowMacros: true} result, err := FromConfluenceStorageWithOptions(input, opts) - require.NoError(t, err) + testutil.RequireNoError(t, err) // All macros should be present - assert.Contains(t, result, "[INFO]") - assert.Contains(t, result, "[TOC]") - assert.Contains(t, result, "[/INFO]") + testutil.Contains(t, result, "[INFO]") + testutil.Contains(t, result, "[TOC]") + testutil.Contains(t, result, "[/INFO]") // Content should be preserved - assert.Contains(t, result, "First paragraph") - assert.Contains(t, result, "Middle text") - assert.Contains(t, result, "Last paragraph") + testutil.Contains(t, result, "First paragraph") + testutil.Contains(t, result, "Middle text") + testutil.Contains(t, result, "Last paragraph") } diff --git a/tools/cfl/pkg/md/macro_test.go b/tools/cfl/pkg/md/macro_test.go index 01b270f..39c6457 100644 --- a/tools/cfl/pkg/md/macro_test.go +++ b/tools/cfl/pkg/md/macro_test.go @@ -1,9 +1,10 @@ package md import ( + "fmt" "testing" - "github.com/stretchr/testify/assert" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestMacroRegistry_ContainsExpectedMacros(t *testing.T) { @@ -12,8 +13,8 @@ func TestMacroRegistry_ContainsExpectedMacros(t *testing.T) { for _, name := range expectedMacros { t.Run(name, func(t *testing.T) { mt, ok := MacroRegistry[name] - assert.True(t, ok, "MacroRegistry should contain %q", name) - assert.Equal(t, name, mt.Name) + testutil.True(t, ok, fmt.Sprintf("MacroRegistry should contain %q", name)) + testutil.Equal(t, name, mt.Name) }) } } @@ -36,9 +37,9 @@ func TestLookupMacro_CaseInsensitive(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { mt, ok := LookupMacro(tt.input) - assert.Equal(t, tt.found, ok) + testutil.Equal(t, tt.found, ok) if tt.found { - assert.Equal(t, tt.expected, mt.Name) + testutil.Equal(t, tt.expected, mt.Name) } }) } @@ -62,9 +63,9 @@ func TestMacroType_BodyConfiguration(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mt, ok := MacroRegistry[tt.name] - assert.True(t, ok) - assert.Equal(t, tt.hasBody, mt.HasBody) - assert.Equal(t, tt.bodyType, mt.BodyType) + testutil.True(t, ok) + testutil.Equal(t, tt.hasBody, mt.HasBody) + testutil.Equal(t, tt.bodyType, mt.BodyType) }) } } @@ -78,10 +79,10 @@ func TestMacroNode_Construction(t *testing.T) { Children: nil, } - assert.Equal(t, "info", node.Name) - assert.Equal(t, "Important", node.Parameters["title"]) - assert.Equal(t, "This is the content", node.Body) - assert.Nil(t, node.Children) + testutil.Equal(t, "info", node.Name) + testutil.Equal(t, "Important", node.Parameters["title"]) + testutil.Equal(t, "This is the content", node.Body) + testutil.Nil(t, node.Children) } func TestMacroNode_WithChildren(t *testing.T) { @@ -99,8 +100,8 @@ func TestMacroNode_WithChildren(t *testing.T) { Children: []*MacroNode{child}, } - assert.Equal(t, "expand", parent.Name) - assert.Len(t, parent.Children, 1) - assert.Equal(t, "code", parent.Children[0].Name) - assert.Equal(t, "go", parent.Children[0].Parameters["language"]) + testutil.Equal(t, "expand", parent.Name) + testutil.Len(t, parent.Children, 1) + testutil.Equal(t, "code", parent.Children[0].Name) + testutil.Equal(t, "go", parent.Children[0].Parameters["language"]) } diff --git a/tools/cfl/pkg/md/parser_test.go b/tools/cfl/pkg/md/parser_test.go index 43899d4..898809b 100644 --- a/tools/cfl/pkg/md/parser_test.go +++ b/tools/cfl/pkg/md/parser_test.go @@ -3,77 +3,76 @@ package md import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) // ==================== Bracket Parser Tests ==================== func TestParseBracketMacros_EmptyInput(t *testing.T) { result, err := ParseBracketMacros("") - require.NoError(t, err) - assert.Empty(t, result.Segments) + testutil.RequireNoError(t, err) + testutil.Empty(t, result.Segments) } func TestParseBracketMacros_PlainText(t *testing.T) { result, err := ParseBracketMacros("Hello world") - require.NoError(t, err) - require.Len(t, result.Segments, 1) - assert.Equal(t, SegmentText, result.Segments[0].Type) - assert.Equal(t, "Hello world", result.Segments[0].Text) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) + testutil.Equal(t, SegmentText, result.Segments[0].Type) + testutil.Equal(t, "Hello world", result.Segments[0].Text) } func TestParseBracketMacros_SimpleTOC(t *testing.T) { result, err := ParseBracketMacros("[TOC]") - require.NoError(t, err) - require.Len(t, result.Segments, 1) - assert.Equal(t, SegmentMacro, result.Segments[0].Type) - assert.Equal(t, "toc", result.Segments[0].Macro.Name) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) + testutil.Equal(t, SegmentMacro, result.Segments[0].Type) + testutil.Equal(t, "toc", result.Segments[0].Macro.Name) } func TestParseBracketMacros_TOCWithParams(t *testing.T) { result, err := ParseBracketMacros("[TOC maxLevel=3 minLevel=1]") - require.NoError(t, err) - require.Len(t, result.Segments, 1) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) macro := result.Segments[0].Macro - assert.Equal(t, "toc", macro.Name) - assert.Equal(t, "3", macro.Parameters["maxLevel"]) - assert.Equal(t, "1", macro.Parameters["minLevel"]) + testutil.Equal(t, "toc", macro.Name) + testutil.Equal(t, "3", macro.Parameters["maxLevel"]) + testutil.Equal(t, "1", macro.Parameters["minLevel"]) } func TestParseBracketMacros_PanelWithBody(t *testing.T) { result, err := ParseBracketMacros("[INFO]Content here[/INFO]") - require.NoError(t, err) - require.Len(t, result.Segments, 1) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) macro := result.Segments[0].Macro - assert.Equal(t, "info", macro.Name) - assert.Equal(t, "Content here", macro.Body) + testutil.Equal(t, "info", macro.Name) + testutil.Equal(t, "Content here", macro.Body) } func TestParseBracketMacros_PanelWithTitleAndBody(t *testing.T) { result, err := ParseBracketMacros(`[WARNING title="Watch Out"]Be careful![/WARNING]`) - require.NoError(t, err) - require.Len(t, result.Segments, 1) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) macro := result.Segments[0].Macro - assert.Equal(t, "warning", macro.Name) - assert.Equal(t, "Watch Out", macro.Parameters["title"]) - assert.Equal(t, "Be careful!", macro.Body) + testutil.Equal(t, "warning", macro.Name) + testutil.Equal(t, "Watch Out", macro.Parameters["title"]) + testutil.Equal(t, "Be careful!", macro.Body) } func TestParseBracketMacros_NestedMacros(t *testing.T) { result, err := ParseBracketMacros("[INFO]Before [TOC] after[/INFO]") - require.NoError(t, err) - require.Len(t, result.Segments, 1) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) macro := result.Segments[0].Macro - assert.Equal(t, "info", macro.Name) + testutil.Equal(t, "info", macro.Name) // TOC should be a child - require.Len(t, macro.Children, 1) - assert.Equal(t, "toc", macro.Children[0].Name) + testutil.Len(t, macro.Children, 1) + testutil.Equal(t, "toc", macro.Children[0].Name) } func TestParseBracketMacros_MultipleMacros(t *testing.T) { result, err := ParseBracketMacros("Before [TOC] middle [INFO]content[/INFO] after") - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should have: text, macro, text, macro, text textCount := 0 @@ -85,15 +84,15 @@ func TestParseBracketMacros_MultipleMacros(t *testing.T) { macroCount++ } } - assert.Equal(t, 2, macroCount, "should have 2 macros") - assert.GreaterOrEqual(t, textCount, 2, "should have at least 2 text segments") + testutil.Equal(t, 2, macroCount) + testutil.GreaterOrEqual(t, textCount, 2) } func TestParseBracketMacros_UnknownMacro(t *testing.T) { result, err := ParseBracketMacros("[UNKNOWN]content[/UNKNOWN]") - require.NoError(t, err) + testutil.RequireNoError(t, err) // Unknown macro should be treated as text - assert.GreaterOrEqual(t, len(result.Warnings), 1, "should have warning") + testutil.GreaterOrEqual(t, len(result.Warnings), 1) // Content should be in text segments hasText := false for _, seg := range result.Segments { @@ -101,19 +100,19 @@ func TestParseBracketMacros_UnknownMacro(t *testing.T) { hasText = true } } - assert.True(t, hasText, "unknown macro should be preserved as text") + testutil.True(t, hasText, "unknown macro should be preserved as text") } func TestParseBracketMacros_MismatchedClose(t *testing.T) { result, err := ParseBracketMacros("[INFO]content[/WARNING]more[/INFO]") - require.NoError(t, err) - assert.GreaterOrEqual(t, len(result.Warnings), 1, "should have warning for mismatch") + testutil.RequireNoError(t, err) + testutil.GreaterOrEqual(t, len(result.Warnings), 1) } func TestParseBracketMacros_UnclosedMacro(t *testing.T) { result, err := ParseBracketMacros("[INFO]content without close") - require.NoError(t, err) - assert.GreaterOrEqual(t, len(result.Warnings), 1, "should have warning for unclosed") + testutil.RequireNoError(t, err) + testutil.GreaterOrEqual(t, len(result.Warnings), 1) // Should be treated as text hasText := false for _, seg := range result.Segments { @@ -121,81 +120,81 @@ func TestParseBracketMacros_UnclosedMacro(t *testing.T) { hasText = true } } - assert.True(t, hasText, "unclosed macro should be preserved as text") + testutil.True(t, hasText, "unclosed macro should be preserved as text") } // ==================== XML Parser Tests ==================== func TestParseConfluenceXML_EmptyInput(t *testing.T) { result, err := ParseConfluenceXML("") - require.NoError(t, err) - assert.Empty(t, result.Segments) + testutil.RequireNoError(t, err) + testutil.Empty(t, result.Segments) } func TestParseConfluenceXML_PlainHTML(t *testing.T) { result, err := ParseConfluenceXML("

    Hello world

    ") - require.NoError(t, err) - require.Len(t, result.Segments, 1) - assert.Equal(t, SegmentText, result.Segments[0].Type) - assert.Equal(t, "

    Hello world

    ", result.Segments[0].Text) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) + testutil.Equal(t, SegmentText, result.Segments[0].Type) + testutil.Equal(t, "

    Hello world

    ", result.Segments[0].Text) } func TestParseConfluenceXML_SimpleTOC(t *testing.T) { input := `` result, err := ParseConfluenceXML(input) - require.NoError(t, err) - require.Len(t, result.Segments, 1) - assert.Equal(t, SegmentMacro, result.Segments[0].Type) - assert.Equal(t, "toc", result.Segments[0].Macro.Name) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) + testutil.Equal(t, SegmentMacro, result.Segments[0].Type) + testutil.Equal(t, "toc", result.Segments[0].Macro.Name) } func TestParseConfluenceXML_TOCWithParams(t *testing.T) { input := `31` result, err := ParseConfluenceXML(input) - require.NoError(t, err) - require.Len(t, result.Segments, 1) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) macro := result.Segments[0].Macro - assert.Equal(t, "toc", macro.Name) - assert.Equal(t, "3", macro.Parameters["maxLevel"]) - assert.Equal(t, "1", macro.Parameters["minLevel"]) + testutil.Equal(t, "toc", macro.Name) + testutil.Equal(t, "3", macro.Parameters["maxLevel"]) + testutil.Equal(t, "1", macro.Parameters["minLevel"]) } func TestParseConfluenceXML_PanelWithBody(t *testing.T) { input := `

    Content

    ` result, err := ParseConfluenceXML(input) - require.NoError(t, err) - require.Len(t, result.Segments, 1) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) macro := result.Segments[0].Macro - assert.Equal(t, "info", macro.Name) - assert.Contains(t, macro.Body, "Content") + testutil.Equal(t, "info", macro.Name) + testutil.Contains(t, macro.Body, "Content") } func TestParseConfluenceXML_CodeWithCDATA(t *testing.T) { input := `python` result, err := ParseConfluenceXML(input) - require.NoError(t, err) - require.Len(t, result.Segments, 1) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) macro := result.Segments[0].Macro - assert.Equal(t, "code", macro.Name) - assert.Equal(t, "python", macro.Parameters["language"]) - assert.Contains(t, macro.Body, `print("Hello")`) + testutil.Equal(t, "code", macro.Name) + testutil.Equal(t, "python", macro.Parameters["language"]) + testutil.Contains(t, macro.Body, `print("Hello")`) } func TestParseConfluenceXML_NestedMacros(t *testing.T) { input := `` result, err := ParseConfluenceXML(input) - require.NoError(t, err) - require.Len(t, result.Segments, 1) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Segments, 1) macro := result.Segments[0].Macro - assert.Equal(t, "info", macro.Name) - require.Len(t, macro.Children, 1) - assert.Equal(t, "toc", macro.Children[0].Name) + testutil.Equal(t, "info", macro.Name) + testutil.Len(t, macro.Children, 1) + testutil.Equal(t, "toc", macro.Children[0].Name) } func TestParseConfluenceXML_WithSurroundingHTML(t *testing.T) { input := `

    Title

    Content

    ` result, err := ParseConfluenceXML(input) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should have text, macro, text textCount := 0 @@ -207,8 +206,8 @@ func TestParseConfluenceXML_WithSurroundingHTML(t *testing.T) { macroCount++ } } - assert.Equal(t, 1, macroCount) - assert.Equal(t, 2, textCount) + testutil.Equal(t, 1, macroCount) + testutil.Equal(t, 2, textCount) } // ==================== Segment Tests ==================== @@ -221,9 +220,9 @@ func TestParseResult_GetMacros(t *testing.T) { result.AddMacroSegment(&MacroNode{Name: "info"}) macros := result.GetMacros() - require.Len(t, macros, 2) - assert.Equal(t, "toc", macros[0].Name) - assert.Equal(t, "info", macros[1].Name) + testutil.Len(t, macros, 2) + testutil.Equal(t, "toc", macros[0].Name) + testutil.Equal(t, "info", macros[1].Name) } func TestParseResult_MergeAdjacentText(t *testing.T) { @@ -232,6 +231,6 @@ func TestParseResult_MergeAdjacentText(t *testing.T) { result.AddTextSegment("world") // Should be merged into single segment - require.Len(t, result.Segments, 1) - assert.Equal(t, "hello world", result.Segments[0].Text) + testutil.Len(t, result.Segments, 1) + testutil.Equal(t, "hello world", result.Segments[0].Text) } diff --git a/tools/cfl/pkg/md/render_test.go b/tools/cfl/pkg/md/render_test.go index b89d887..b70a29f 100644 --- a/tools/cfl/pkg/md/render_test.go +++ b/tools/cfl/pkg/md/render_test.go @@ -4,16 +4,16 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestRenderMacroToXML_SimpleTOC(t *testing.T) { node := &MacroNode{Name: "toc"} xml := RenderMacroToXML(node) - assert.Contains(t, xml, `ac:name="toc"`) - assert.Contains(t, xml, `ac:schema-version="1"`) - assert.Contains(t, xml, `
    `) + testutil.Contains(t, xml, `ac:name="toc"`) + testutil.Contains(t, xml, `ac:schema-version="1"`) + testutil.Contains(t, xml, `
    `) } func TestRenderMacroToXML_TOCWithParams(t *testing.T) { @@ -23,8 +23,8 @@ func TestRenderMacroToXML_TOCWithParams(t *testing.T) { } xml := RenderMacroToXML(node) - assert.Contains(t, xml, `3`) - assert.Contains(t, xml, `1`) + testutil.Contains(t, xml, `3`) + testutil.Contains(t, xml, `1`) } func TestRenderMacroToXML_PanelWithBody(t *testing.T) { @@ -34,10 +34,10 @@ func TestRenderMacroToXML_PanelWithBody(t *testing.T) { } xml := RenderMacroToXML(node) - assert.Contains(t, xml, `ac:name="info"`) - assert.Contains(t, xml, ``) - assert.Contains(t, xml, `

    Content

    `) - assert.Contains(t, xml, `
    `) + testutil.Contains(t, xml, `ac:name="info"`) + testutil.Contains(t, xml, ``) + testutil.Contains(t, xml, `

    Content

    `) + testutil.Contains(t, xml, `
    `) } func TestRenderMacroToXML_CodeWithCDATA(t *testing.T) { @@ -48,10 +48,10 @@ func TestRenderMacroToXML_CodeWithCDATA(t *testing.T) { } xml := RenderMacroToXML(node) - assert.Contains(t, xml, `ac:name="code"`) - assert.Contains(t, xml, ``) + testutil.Contains(t, xml, `ac:name="code"`) + testutil.Contains(t, xml, ``) } func TestRenderMacroToXML_EscapesXML(t *testing.T) { @@ -61,14 +61,14 @@ func TestRenderMacroToXML_EscapesXML(t *testing.T) { } xml := RenderMacroToXML(node) - assert.Contains(t, xml, `A & B <test>`) + testutil.Contains(t, xml, `A & B <test>`) } func TestRenderMacroToBracket_SimpleTOC(t *testing.T) { node := &MacroNode{Name: "toc"} bracket := RenderMacroToBracket(node) - assert.Equal(t, "[TOC]", bracket) + testutil.Equal(t, "[TOC]", bracket) } func TestRenderMacroToBracket_TOCWithParams(t *testing.T) { @@ -78,9 +78,9 @@ func TestRenderMacroToBracket_TOCWithParams(t *testing.T) { } bracket := RenderMacroToBracket(node) - assert.Contains(t, bracket, "[TOC") - assert.Contains(t, bracket, "maxLevel=3") - assert.Contains(t, bracket, "]") + testutil.Contains(t, bracket, "[TOC") + testutil.Contains(t, bracket, "maxLevel=3") + testutil.Contains(t, bracket, "]") } func TestRenderMacroToBracket_PanelWithBody(t *testing.T) { @@ -91,10 +91,10 @@ func TestRenderMacroToBracket_PanelWithBody(t *testing.T) { } bracket := RenderMacroToBracket(node) - assert.Contains(t, bracket, "[INFO") - assert.Contains(t, bracket, "title=Important") - assert.Contains(t, bracket, "Content here") - assert.Contains(t, bracket, "[/INFO]") + testutil.Contains(t, bracket, "[INFO") + testutil.Contains(t, bracket, "title=Important") + testutil.Contains(t, bracket, "Content here") + testutil.Contains(t, bracket, "[/INFO]") } func TestRenderMacroToBracket_QuotedValues(t *testing.T) { @@ -104,13 +104,13 @@ func TestRenderMacroToBracket_QuotedValues(t *testing.T) { } bracket := RenderMacroToBracket(node) - assert.Contains(t, bracket, `title="Hello World"`) + testutil.Contains(t, bracket, `title="Hello World"`) } func TestRenderMacroToBracketOpen_SimpleTOC(t *testing.T) { node := &MacroNode{Name: "toc"} bracket := RenderMacroToBracketOpen(node) - assert.Equal(t, "[TOC]", bracket) + testutil.Equal(t, "[TOC]", bracket) } func TestRenderMacroToBracketOpen_WithParams(t *testing.T) { @@ -119,12 +119,12 @@ func TestRenderMacroToBracketOpen_WithParams(t *testing.T) { Parameters: map[string]string{"title": "Hello World"}, } bracket := RenderMacroToBracketOpen(node) - assert.Contains(t, bracket, "[INFO") - assert.Contains(t, bracket, `title="Hello World"`) - assert.True(t, strings.HasSuffix(bracket, "]")) + testutil.Contains(t, bracket, "[INFO") + testutil.Contains(t, bracket, `title="Hello World"`) + testutil.True(t, strings.HasSuffix(bracket, "]")) } func TestFormatPlaceholder(t *testing.T) { - assert.Equal(t, "CFMACRO0END", FormatPlaceholder(0)) - assert.Equal(t, "CFMACRO42END", FormatPlaceholder(42)) + testutil.Equal(t, "CFMACRO0END", FormatPlaceholder(0)) + testutil.Equal(t, "CFMACRO42END", FormatPlaceholder(42)) } diff --git a/tools/cfl/pkg/md/roundtrip_test.go b/tools/cfl/pkg/md/roundtrip_test.go index 2af7104..ef34652 100644 --- a/tools/cfl/pkg/md/roundtrip_test.go +++ b/tools/cfl/pkg/md/roundtrip_test.go @@ -4,8 +4,7 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) // TestRoundtrip verifies that macros survive MD→XHTML→MD conversion. @@ -14,15 +13,15 @@ func TestRoundtrip_TOC(t *testing.T) { // MD → XHTML xhtml, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) - assert.Contains(t, xhtml, `ac:name="toc"`) - assert.Contains(t, xhtml, `maxLevel`) + testutil.RequireNoError(t, err) + testutil.Contains(t, xhtml, `ac:name="toc"`) + testutil.Contains(t, xhtml, `maxLevel`) // XHTML → MD md, err := FromConfluenceStorageWithOptions(xhtml, ConvertOptions{ShowMacros: true}) - require.NoError(t, err) - assert.Contains(t, strings.ToUpper(md), "[TOC") - assert.Contains(t, md, "maxLevel") + testutil.RequireNoError(t, err) + testutil.Contains(t, strings.ToUpper(md), "[TOC") + testutil.Contains(t, md, "maxLevel") } func TestRoundtrip_InfoPanel(t *testing.T) { @@ -32,16 +31,16 @@ This is important content. // MD → XHTML xhtml, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) - assert.Contains(t, xhtml, `ac:name="info"`) - assert.Contains(t, xhtml, ``) + testutil.RequireNoError(t, err) + testutil.Contains(t, xhtml, `ac:name="info"`) + testutil.Contains(t, xhtml, ``) // XHTML → MD md, err := FromConfluenceStorageWithOptions(xhtml, ConvertOptions{ShowMacros: true}) - require.NoError(t, err) - assert.Contains(t, strings.ToUpper(md), "[INFO") - assert.Contains(t, md, "[/INFO]") - assert.Contains(t, md, "important content") + testutil.RequireNoError(t, err) + testutil.Contains(t, strings.ToUpper(md), "[INFO") + testutil.Contains(t, md, "[/INFO]") + testutil.Contains(t, md, "important content") } func TestRoundtrip_NestedMacros(t *testing.T) { @@ -51,15 +50,15 @@ Content with [TOC] inside. // MD → XHTML xhtml, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) - assert.Contains(t, xhtml, `ac:name="info"`) - assert.Contains(t, xhtml, `ac:name="toc"`) + testutil.RequireNoError(t, err) + testutil.Contains(t, xhtml, `ac:name="info"`) + testutil.Contains(t, xhtml, `ac:name="toc"`) // XHTML → MD md, err := FromConfluenceStorageWithOptions(xhtml, ConvertOptions{ShowMacros: true}) - require.NoError(t, err) - assert.Contains(t, strings.ToUpper(md), "[INFO") - assert.Contains(t, strings.ToUpper(md), "[TOC") + testutil.RequireNoError(t, err) + testutil.Contains(t, strings.ToUpper(md), "[INFO") + testutil.Contains(t, strings.ToUpper(md), "[TOC") } func TestRoundtrip_AllPanelTypes(t *testing.T) { @@ -70,13 +69,13 @@ func TestRoundtrip_AllPanelTypes(t *testing.T) { input := "[" + pt + "]Content[/" + pt + "]" xhtml, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) - assert.Contains(t, xhtml, `ac:name="`+strings.ToLower(pt)+`"`) + testutil.RequireNoError(t, err) + testutil.Contains(t, xhtml, `ac:name="`+strings.ToLower(pt)+`"`) md, err := FromConfluenceStorageWithOptions(xhtml, ConvertOptions{ShowMacros: true}) - require.NoError(t, err) - assert.Contains(t, strings.ToUpper(md), "["+pt) - assert.Contains(t, strings.ToUpper(md), "[/"+pt+"]") + testutil.RequireNoError(t, err) + testutil.Contains(t, strings.ToUpper(md), "["+pt) + testutil.Contains(t, strings.ToUpper(md), "[/"+pt+"]") }) } } @@ -92,23 +91,23 @@ After // MD → XHTML xhtml, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // XHTML → MD md, err := FromConfluenceStorageWithOptions(xhtml, ConvertOptions{ShowMacros: true}) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify order is preserved: Before < TOC < After beforeIdx := strings.Index(md, "Before") tocIdx := strings.Index(strings.ToUpper(md), "[TOC") afterIdx := strings.Index(md, "After") - assert.True(t, beforeIdx >= 0, "Before should be present") - assert.True(t, tocIdx >= 0, "TOC should be present") - assert.True(t, afterIdx >= 0, "After should be present") + testutil.True(t, beforeIdx >= 0, "Before should be present") + testutil.True(t, tocIdx >= 0, "TOC should be present") + testutil.True(t, afterIdx >= 0, "After should be present") - assert.True(t, beforeIdx < tocIdx, "Before should come before TOC") - assert.True(t, tocIdx < afterIdx, "TOC should come before After") + testutil.True(t, beforeIdx < tocIdx, "Before should come before TOC") + testutil.True(t, tocIdx < afterIdx, "TOC should come before After") } func TestRoundtrip_MultipleNestedMacros(t *testing.T) { @@ -122,25 +121,25 @@ End // MD → XHTML xhtml, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // XHTML → MD md, err := FromConfluenceStorageWithOptions(xhtml, ConvertOptions{ShowMacros: true}) - require.NoError(t, err) + testutil.RequireNoError(t, err) // All text and macros should be present - assert.Contains(t, md, "Start") - assert.Contains(t, md, "Middle") - assert.Contains(t, md, "End") - assert.Contains(t, strings.ToUpper(md), "[TOC") + testutil.Contains(t, md, "Start") + testutil.Contains(t, md, "Middle") + testutil.Contains(t, md, "End") + testutil.Contains(t, strings.ToUpper(md), "[TOC") // Verify order startIdx := strings.Index(md, "Start") middleIdx := strings.Index(md, "Middle") endIdx := strings.Index(md, "End") - assert.True(t, startIdx < middleIdx, "Start should come before Middle") - assert.True(t, middleIdx < endIdx, "Middle should come before End") + testutil.True(t, startIdx < middleIdx, "Start should come before Middle") + testutil.True(t, middleIdx < endIdx, "Middle should come before End") } func TestRoundtrip_DeeplyNested(t *testing.T) { @@ -156,23 +155,23 @@ More outer // MD → XHTML xhtml, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify nesting in XHTML - assert.Contains(t, xhtml, `ac:name="info"`) - assert.Contains(t, xhtml, `ac:name="warning"`) - assert.Contains(t, xhtml, `ac:name="toc"`) + testutil.Contains(t, xhtml, `ac:name="info"`) + testutil.Contains(t, xhtml, `ac:name="warning"`) + testutil.Contains(t, xhtml, `ac:name="toc"`) // XHTML → MD md, err := FromConfluenceStorageWithOptions(xhtml, ConvertOptions{ShowMacros: true}) - require.NoError(t, err) + testutil.RequireNoError(t, err) // All elements should be present - assert.Contains(t, md, "Outer") - assert.Contains(t, strings.ToUpper(md), "[INFO") - assert.Contains(t, strings.ToUpper(md), "[WARNING") - assert.Contains(t, strings.ToUpper(md), "[TOC") - assert.Contains(t, md, "Inner") + testutil.Contains(t, md, "Outer") + testutil.Contains(t, strings.ToUpper(md), "[INFO") + testutil.Contains(t, strings.ToUpper(md), "[WARNING") + testutil.Contains(t, strings.ToUpper(md), "[TOC") + testutil.Contains(t, md, "Inner") } // TestRoundtrip_CloseTagNotDuplicated verifies that panel content appears exactly once @@ -182,17 +181,17 @@ func TestRoundtrip_CloseTagNotDuplicated(t *testing.T) { // MD → XHTML xhtml, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Content should appear exactly once in XHTML - assert.Equal(t, 1, strings.Count(xhtml, "unique content")) + testutil.Equal(t, 1, strings.Count(xhtml, "unique content")) // XHTML → MD md, err := FromConfluenceStorageWithOptions(xhtml, ConvertOptions{ShowMacros: true}) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Content should appear exactly once in MD - assert.Equal(t, 1, strings.Count(md, "unique content")) + testutil.Equal(t, 1, strings.Count(md, "unique content")) } // TestRoundtrip_NestedMacroInParagraph verifies the issue #56 fix. @@ -204,28 +203,28 @@ func TestRoundtrip_NestedMacroInParagraph(t *testing.T) { // Convert to XHTML xhtml, err := ToConfluenceStorage([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify XHTML has correct structure - assert.Contains(t, xhtml, "ac:structured-macro") - assert.Contains(t, xhtml, `ac:name="info"`) - assert.Contains(t, xhtml, `ac:name="toc"`) + testutil.Contains(t, xhtml, "ac:structured-macro") + testutil.Contains(t, xhtml, `ac:name="info"`) + testutil.Contains(t, xhtml, `ac:name="toc"`) // Convert back to markdown md, err := FromConfluenceStorageWithOptions(xhtml, ConvertOptions{ShowMacros: true}) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify structure preserved (case-insensitive check for macro names) - assert.Contains(t, strings.ToUpper(md), "[INFO]") - assert.Contains(t, strings.ToUpper(md), "[TOC]") - assert.Contains(t, strings.ToUpper(md), "[/INFO]") - assert.Contains(t, md, "# Header 1") + testutil.Contains(t, strings.ToUpper(md), "[INFO]") + testutil.Contains(t, strings.ToUpper(md), "[TOC]") + testutil.Contains(t, strings.ToUpper(md), "[/INFO]") + testutil.Contains(t, md, "# Header 1") // Verify nesting order is preserved infoStart := strings.Index(strings.ToUpper(md), "[INFO]") tocPos := strings.Index(strings.ToUpper(md), "[TOC]") infoEnd := strings.Index(strings.ToUpper(md), "[/INFO]") - assert.True(t, infoStart < tocPos, "[INFO] should come before [TOC]") - assert.True(t, tocPos < infoEnd, "[TOC] should come before [/INFO]") + testutil.True(t, infoStart < tocPos, "[INFO] should come before [TOC]") + testutil.True(t, tocPos < infoEnd, "[TOC] should come before [/INFO]") } diff --git a/tools/cfl/pkg/md/to_adf_test.go b/tools/cfl/pkg/md/to_adf_test.go index b9e4a9c..1eae0e8 100644 --- a/tools/cfl/pkg/md/to_adf_test.go +++ b/tools/cfl/pkg/md/to_adf_test.go @@ -2,30 +2,30 @@ package md import ( "encoding/json" + "fmt" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestToADF_Paragraph(t *testing.T) { input := "Hello world" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, "doc", doc.Type) - assert.Equal(t, 1, doc.Version) - require.Len(t, doc.Content, 1) + testutil.Equal(t, "doc", doc.Type) + testutil.Equal(t, 1, doc.Version) + testutil.Len(t, doc.Content, 1) para := doc.Content[0] - assert.Equal(t, "paragraph", para.Type) - require.Len(t, para.Content, 1) - assert.Equal(t, "text", para.Content[0].Type) - assert.Equal(t, "Hello world", para.Content[0].Text) + testutil.Equal(t, "paragraph", para.Type) + testutil.Len(t, para.Content, 1) + testutil.Equal(t, "text", para.Content[0].Type) + testutil.Equal(t, "Hello world", para.Content[0].Text) } func TestToADF_Headings(t *testing.T) { @@ -46,18 +46,18 @@ func TestToADF_Headings(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToADF([]byte(tt.markdown)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) heading := doc.Content[0] - assert.Equal(t, "heading", heading.Type) - assert.EqualValues(t, tt.level, heading.Attrs["level"]) - require.Len(t, heading.Content, 1) - assert.Equal(t, tt.text, heading.Content[0].Text) + testutil.Equal(t, "heading", heading.Type) + testutil.Equal(t, heading.Attrs["level"], float64(tt.level)) + testutil.Len(t, heading.Content, 1) + testutil.Equal(t, tt.text, heading.Content[0].Text) }) } } @@ -77,15 +77,15 @@ func TestToADF_Formatting(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToADF([]byte(tt.markdown)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) para := doc.Content[0] - assert.Equal(t, "paragraph", para.Type) + testutil.Equal(t, "paragraph", para.Type) // Find the text node with marks var foundMark bool @@ -99,7 +99,7 @@ func TestToADF_Formatting(t *testing.T) { } } } - assert.True(t, foundMark, "expected to find mark %s", tt.mark) + testutil.True(t, foundMark, fmt.Sprintf("expected to find mark %s", tt.mark)) }) } } @@ -107,13 +107,13 @@ func TestToADF_Formatting(t *testing.T) { func TestToADF_Links(t *testing.T) { input := "[Example](https://example.com)" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) para := doc.Content[0] // Find the link @@ -122,53 +122,53 @@ func TestToADF_Links(t *testing.T) { for _, mark := range node.Marks { if mark.Type == "link" { foundLink = true - assert.Equal(t, "https://example.com", mark.Attrs["href"]) - assert.Equal(t, "Example", node.Text) + testutil.Equal(t, "https://example.com", mark.Attrs["href"]) + testutil.Equal(t, "Example", node.Text) } } } - assert.True(t, foundLink, "expected to find link mark") + testutil.True(t, foundLink, "expected to find link mark") } func TestToADF_BulletList(t *testing.T) { input := "- Item one\n- Item two\n- Item three" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) list := doc.Content[0] - assert.Equal(t, "bulletList", list.Type) - assert.Len(t, list.Content, 3) + testutil.Equal(t, "bulletList", list.Type) + testutil.Len(t, list.Content, 3) for i, item := range list.Content { - assert.Equal(t, "listItem", item.Type) - require.Len(t, item.Content, 1) + testutil.Equal(t, "listItem", item.Type) + testutil.Len(t, item.Content, 1) para := item.Content[0] - assert.Equal(t, "paragraph", para.Type) + testutil.Equal(t, "paragraph", para.Type) expected := []string{"Item one", "Item two", "Item three"}[i] - require.Len(t, para.Content, 1) - assert.Equal(t, expected, para.Content[0].Text) + testutil.Len(t, para.Content, 1) + testutil.Equal(t, expected, para.Content[0].Text) } } func TestToADF_OrderedList(t *testing.T) { input := "1. First\n2. Second\n3. Third" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) list := doc.Content[0] - assert.Equal(t, "orderedList", list.Type) - assert.EqualValues(t, 1, list.Attrs["order"]) - assert.Len(t, list.Content, 3) + testutil.Equal(t, "orderedList", list.Type) + testutil.Equal(t, list.Attrs["order"], float64(1)) + testutil.Len(t, list.Content, 3) } func TestToADF_CodeBlock(t *testing.T) { @@ -201,22 +201,22 @@ func TestToADF_CodeBlock(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToADF([]byte(tt.markdown)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) block := doc.Content[0] - assert.Equal(t, "codeBlock", block.Type) + testutil.Equal(t, "codeBlock", block.Type) if tt.language != "" { - assert.Equal(t, tt.language, block.Attrs["language"]) + testutil.Equal(t, tt.language, block.Attrs["language"]) } - require.Len(t, block.Content, 1) - assert.Equal(t, tt.code, block.Content[0].Text) + testutil.Len(t, block.Content, 1) + testutil.Equal(t, tt.code, block.Content[0].Text) }) } } @@ -224,114 +224,114 @@ func TestToADF_CodeBlock(t *testing.T) { func TestToADF_Blockquote(t *testing.T) { input := "> This is a quote" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) quote := doc.Content[0] - assert.Equal(t, "blockquote", quote.Type) - require.Len(t, quote.Content, 1) - assert.Equal(t, "paragraph", quote.Content[0].Type) + testutil.Equal(t, "blockquote", quote.Type) + testutil.Len(t, quote.Content, 1) + testutil.Equal(t, "paragraph", quote.Content[0].Type) } func TestToADF_HorizontalRule(t *testing.T) { input := "Above\n\n---\n\nBelow" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Len(t, doc.Content, 3) - assert.Equal(t, "paragraph", doc.Content[0].Type) - assert.Equal(t, "rule", doc.Content[1].Type) - assert.Equal(t, "paragraph", doc.Content[2].Type) + testutil.Len(t, doc.Content, 3) + testutil.Equal(t, "paragraph", doc.Content[0].Type) + testutil.Equal(t, "rule", doc.Content[1].Type) + testutil.Equal(t, "paragraph", doc.Content[2].Type) } func TestToADF_Table(t *testing.T) { input := "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) table := doc.Content[0] - assert.Equal(t, "table", table.Type) + testutil.Equal(t, "table", table.Type) // Should have 2 rows (header + 1 data row) - assert.Len(t, table.Content, 2) + testutil.Len(t, table.Content, 2) // First row should have tableHeader cells headerRow := table.Content[0] - assert.Equal(t, "tableRow", headerRow.Type) - assert.Len(t, headerRow.Content, 2) - assert.Equal(t, "tableHeader", headerRow.Content[0].Type) + testutil.Equal(t, "tableRow", headerRow.Type) + testutil.Len(t, headerRow.Content, 2) + testutil.Equal(t, "tableHeader", headerRow.Content[0].Type) // Second row should have tableCell cells dataRow := table.Content[1] - assert.Equal(t, "tableRow", dataRow.Type) - assert.Len(t, dataRow.Content, 2) - assert.Equal(t, "tableCell", dataRow.Content[0].Type) + testutil.Equal(t, "tableRow", dataRow.Type) + testutil.Len(t, dataRow.Content, 2) + testutil.Equal(t, "tableCell", dataRow.Content[0].Type) } func TestToADF_EmptyInput(t *testing.T) { result, err := ToADF([]byte("")) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - assert.Equal(t, "doc", doc.Type) - assert.Equal(t, 1, doc.Version) - assert.Empty(t, doc.Content) + testutil.Equal(t, "doc", doc.Type) + testutil.Equal(t, 1, doc.Version) + testutil.Empty(t, doc.Content) } func TestToADF_NestedList(t *testing.T) { input := "- Item one\n - Nested one\n - Nested two\n- Item two" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) list := doc.Content[0] - assert.Equal(t, "bulletList", list.Type) + testutil.Equal(t, "bulletList", list.Type) // First list item should contain a nested bulletList firstItem := list.Content[0] - assert.Equal(t, "listItem", firstItem.Type) + testutil.Equal(t, "listItem", firstItem.Type) // Should have paragraph + nested list var foundNestedList bool for _, child := range firstItem.Content { if child.Type == "bulletList" { foundNestedList = true - assert.Len(t, child.Content, 2) // Two nested items + testutil.Len(t, child.Content, 2) // Two nested items } } - assert.True(t, foundNestedList, "expected nested bullet list") + testutil.True(t, foundNestedList, "expected nested bullet list") } func TestToADF_BoldAndItalicCombined(t *testing.T) { input := "***bold and italic***" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) para := doc.Content[0] // Find the text node with both marks @@ -346,8 +346,8 @@ func TestToADF_BoldAndItalicCombined(t *testing.T) { } } } - assert.True(t, foundStrong, "expected strong mark") - assert.True(t, foundEm, "expected em mark") + testutil.True(t, foundStrong, "expected strong mark") + testutil.True(t, foundEm, "expected em mark") } func TestToADF_OutputIsValidJSON(t *testing.T) { @@ -362,88 +362,88 @@ func TestToADF_OutputIsValidJSON(t *testing.T) { for _, input := range inputs { result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Verify it's valid JSON var parsed map[string]interface{} err = json.Unmarshal([]byte(result), &parsed) - require.NoError(t, err, "Output should be valid JSON for input: %s", input) + testutil.RequireNoError(t, err) // Verify basic structure - assert.Equal(t, "doc", parsed["type"]) - assert.EqualValues(t, 1, parsed["version"]) + testutil.Equal(t, "doc", parsed["type"]) + testutil.Equal(t, parsed["version"], float64(1)) } } func TestToADF_Images_AltText(t *testing.T) { input := "![Alt text](https://example.com/image.png)" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Images should be converted to text with alt text - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) para := doc.Content[0] - assert.Equal(t, "paragraph", para.Type) - require.Len(t, para.Content, 1) - assert.Equal(t, "Alt text", para.Content[0].Text) + testutil.Equal(t, "paragraph", para.Type) + testutil.Len(t, para.Content, 1) + testutil.Equal(t, "Alt text", para.Content[0].Text) } func TestToADF_WhitespaceInCodeBlock(t *testing.T) { // Code with leading whitespace should be preserved input := "```\n indented code\n more indented\n```" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) block := doc.Content[0] - assert.Equal(t, "codeBlock", block.Type) - require.Len(t, block.Content, 1) + testutil.Equal(t, "codeBlock", block.Type) + testutil.Len(t, block.Content, 1) // Verify whitespace is preserved text := block.Content[0].Text - assert.Contains(t, text, " indented") - assert.Contains(t, text, " more indented") + testutil.Contains(t, text, " indented") + testutil.Contains(t, text, " more indented") } func TestToADF_NestedBlockquote(t *testing.T) { input := "> Quote with **bold** text\n>\n> And a list:\n> - Item 1\n> - Item 2" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) quote := doc.Content[0] - assert.Equal(t, "blockquote", quote.Type) + testutil.Equal(t, "blockquote", quote.Type) // Should have nested content - assert.True(t, len(quote.Content) > 0, "blockquote should have content") + testutil.True(t, len(quote.Content) > 0, "blockquote should have content") } func TestToADF_HardLineBreak(t *testing.T) { // Two spaces at end of line creates a hard break input := "Line one \nLine two" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should have paragraph with hard break - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) para := doc.Content[0] - assert.Equal(t, "paragraph", para.Type) + testutil.Equal(t, "paragraph", para.Type) // Check for hardBreak node or separate text nodes var foundBreak bool @@ -460,21 +460,21 @@ func TestToADF_HardLineBreak(t *testing.T) { for _, node := range para.Content { fullText += node.Text } - assert.Contains(t, fullText, "Line one") - assert.Contains(t, fullText, "Line two") + testutil.Contains(t, fullText, "Line one") + testutil.Contains(t, fullText, "Line two") } } func TestToADF_InlineCodePreservesContent(t *testing.T) { input := "Use `fmt.Println()` to print" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) para := doc.Content[0] // Find the code-marked text @@ -483,11 +483,11 @@ func TestToADF_InlineCodePreservesContent(t *testing.T) { for _, mark := range node.Marks { if mark.Type == "code" { foundCode = true - assert.Equal(t, "fmt.Println()", node.Text) + testutil.Equal(t, "fmt.Println()", node.Text) } } } - assert.True(t, foundCode, "expected code mark") + testutil.True(t, foundCode, "expected code mark") } // --- Macro conversion tests --- @@ -495,68 +495,68 @@ func TestToADF_InlineCodePreservesContent(t *testing.T) { func TestToADF_TOC_Simple(t *testing.T) { input := "[TOC]" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) ext := doc.Content[0] - assert.Equal(t, "extension", ext.Type) - assert.Equal(t, "com.atlassian.confluence.macro.core", ext.Attrs["extensionType"]) - assert.Equal(t, "toc", ext.Attrs["extensionKey"]) - assert.Equal(t, "default", ext.Attrs["layout"]) + testutil.Equal(t, "extension", ext.Type) + testutil.Equal(t, "com.atlassian.confluence.macro.core", ext.Attrs["extensionType"]) + testutil.Equal(t, "toc", ext.Attrs["extensionKey"]) + testutil.Equal(t, "default", ext.Attrs["layout"]) // Verify parameters structure params, ok := ext.Attrs["parameters"].(map[string]interface{}) - require.True(t, ok, "parameters should be a map") + testutil.True(t, ok, "parameters should be a map") metadata, ok := params["macroMetadata"].(map[string]interface{}) - require.True(t, ok, "macroMetadata should be a map") + testutil.True(t, ok, "macroMetadata should be a map") schemaVersion, ok := metadata["schemaVersion"].(map[string]interface{}) - require.True(t, ok, "schemaVersion should be a map") - assert.Equal(t, "1", schemaVersion["value"]) + testutil.True(t, ok, "schemaVersion should be a map") + testutil.Equal(t, "1", schemaVersion["value"]) } func TestToADF_TOC_WithParams(t *testing.T) { input := "[TOC maxLevel=3]" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) ext := doc.Content[0] - assert.Equal(t, "extension", ext.Type) - assert.Equal(t, "toc", ext.Attrs["extensionKey"]) + testutil.Equal(t, "extension", ext.Type) + testutil.Equal(t, "toc", ext.Attrs["extensionKey"]) // Verify macro param params := ext.Attrs["parameters"].(map[string]interface{}) macroParams := params["macroParams"].(map[string]interface{}) maxLevel := macroParams["maxLevel"].(map[string]interface{}) - assert.Equal(t, "3", maxLevel["value"]) + testutil.Equal(t, "3", maxLevel["value"]) } func TestToADF_TOC_MultipleParams(t *testing.T) { input := "[TOC maxLevel=3 minLevel=1]" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) ext := doc.Content[0] params := ext.Attrs["parameters"].(map[string]interface{}) macroParams := params["macroParams"].(map[string]interface{}) maxLevel := macroParams["maxLevel"].(map[string]interface{}) - assert.Equal(t, "3", maxLevel["value"]) + testutil.Equal(t, "3", maxLevel["value"]) minLevel := macroParams["minLevel"].(map[string]interface{}) - assert.Equal(t, "1", minLevel["value"]) + testutil.Equal(t, "1", minLevel["value"]) } func TestToADF_TOC_CaseInsensitive(t *testing.T) { @@ -572,15 +572,15 @@ func TestToADF_TOC_CaseInsensitive(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ToADF([]byte(tt.input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) - assert.Equal(t, "extension", doc.Content[0].Type) - assert.Equal(t, "toc", doc.Content[0].Attrs["extensionKey"]) + testutil.Len(t, doc.Content, 1) + testutil.Equal(t, "extension", doc.Content[0].Type) + testutil.Equal(t, "toc", doc.Content[0].Attrs["extensionKey"]) }) } } @@ -588,48 +588,48 @@ func TestToADF_TOC_CaseInsensitive(t *testing.T) { func TestToADF_TOC_WithSurroundingContent(t *testing.T) { input := "Before content.\n\n[TOC]\n\n# Heading\n\nAfter content." result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should have: paragraph, extension, heading, paragraph - require.Len(t, doc.Content, 4) - assert.Equal(t, "paragraph", doc.Content[0].Type) - assert.Equal(t, "extension", doc.Content[1].Type) - assert.Equal(t, "toc", doc.Content[1].Attrs["extensionKey"]) - assert.Equal(t, "heading", doc.Content[2].Type) - assert.Equal(t, "paragraph", doc.Content[3].Type) + testutil.Len(t, doc.Content, 4) + testutil.Equal(t, "paragraph", doc.Content[0].Type) + testutil.Equal(t, "extension", doc.Content[1].Type) + testutil.Equal(t, "toc", doc.Content[1].Attrs["extensionKey"]) + testutil.Equal(t, "heading", doc.Content[2].Type) + testutil.Equal(t, "paragraph", doc.Content[3].Type) } func TestToADF_TOC_InsideCodeBlock_Preserved(t *testing.T) { input := "```\n[TOC]\n```" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should be a code block, NOT an extension - require.Len(t, doc.Content, 1) - assert.Equal(t, "codeBlock", doc.Content[0].Type) - assert.Contains(t, doc.Content[0].Content[0].Text, "[TOC]") + testutil.Len(t, doc.Content, 1) + testutil.Equal(t, "codeBlock", doc.Content[0].Type) + testutil.Contains(t, doc.Content[0].Content[0].Text, "[TOC]") } func TestToADF_TOC_InsideInlineCode_Preserved(t *testing.T) { input := "Use `[TOC]` to add a table of contents." result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should be a paragraph with inline code, NOT an extension - require.Len(t, doc.Content, 1) - assert.Equal(t, "paragraph", doc.Content[0].Type) + testutil.Len(t, doc.Content, 1) + testutil.Equal(t, "paragraph", doc.Content[0].Type) // Find the code-marked text containing [TOC] var foundCode bool @@ -640,23 +640,23 @@ func TestToADF_TOC_InsideInlineCode_Preserved(t *testing.T) { } } } - assert.True(t, foundCode, "expected [TOC] as inline code, not a macro") + testutil.True(t, foundCode, "expected [TOC] as inline code, not a macro") } func TestToADF_InfoPanel(t *testing.T) { input := "[INFO]\nThis is important content.\n[/INFO]" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) panel := doc.Content[0] - assert.Equal(t, "panel", panel.Type) - assert.Equal(t, "info", panel.Attrs["panelType"]) - require.True(t, len(panel.Content) > 0, "panel should have body content") + testutil.Equal(t, "panel", panel.Type) + testutil.Equal(t, "info", panel.Attrs["panelType"]) + testutil.True(t, len(panel.Content) > 0, "panel should have body content") // Body should contain the text var foundText bool @@ -669,108 +669,108 @@ func TestToADF_InfoPanel(t *testing.T) { } } } - assert.True(t, foundText, "panel body should contain the text") + testutil.True(t, foundText, "panel body should contain the text") } func TestToADF_WarningPanel(t *testing.T) { input := "[WARNING]\nBe careful!\n[/WARNING]" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) panel := doc.Content[0] - assert.Equal(t, "panel", panel.Type) - assert.Equal(t, "warning", panel.Attrs["panelType"]) + testutil.Equal(t, "panel", panel.Type) + testutil.Equal(t, "warning", panel.Attrs["panelType"]) } func TestToADF_NotePanel(t *testing.T) { input := "[NOTE]\nTake note of this.\n[/NOTE]" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) panel := doc.Content[0] - assert.Equal(t, "panel", panel.Type) - assert.Equal(t, "note", panel.Attrs["panelType"]) + testutil.Equal(t, "panel", panel.Type) + testutil.Equal(t, "note", panel.Attrs["panelType"]) } func TestToADF_TipPanel(t *testing.T) { input := "[TIP]\nHere is a tip.\n[/TIP]" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) panel := doc.Content[0] - assert.Equal(t, "panel", panel.Type) - assert.Equal(t, "success", panel.Attrs["panelType"]) + testutil.Equal(t, "panel", panel.Type) + testutil.Equal(t, "success", panel.Attrs["panelType"]) } func TestToADF_NestedMacro_TOCInsideInfo(t *testing.T) { input := "[INFO]\nContent with [TOC] inside.\n[/INFO]" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) // The outer macro should be a panel - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) panel := doc.Content[0] - assert.Equal(t, "panel", panel.Type) + testutil.Equal(t, "panel", panel.Type) // The panel body may have the TOC placeholder resolved or the text // depending on how deep we recurse. At minimum, the panel should exist. - assert.True(t, len(panel.Content) > 0, "panel should have content") + testutil.True(t, len(panel.Content) > 0, "panel should have content") } func TestToADF_ExpandMacro(t *testing.T) { input := "[EXPAND]\nExpanded content here.\n[/EXPAND]" result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) - require.Len(t, doc.Content, 1) + testutil.Len(t, doc.Content, 1) ext := doc.Content[0] - assert.Equal(t, "bodiedExtension", ext.Type) - assert.Equal(t, "com.atlassian.confluence.macro.core", ext.Attrs["extensionType"]) - assert.Equal(t, "expand", ext.Attrs["extensionKey"]) - require.True(t, len(ext.Content) > 0, "bodied extension should have content") + testutil.Equal(t, "bodiedExtension", ext.Type) + testutil.Equal(t, "com.atlassian.confluence.macro.core", ext.Attrs["extensionType"]) + testutil.Equal(t, "expand", ext.Attrs["extensionKey"]) + testutil.True(t, len(ext.Content) > 0, "bodied extension should have content") } func TestToADF_MultipleMacroTypes(t *testing.T) { input := "[TOC]\n\n# Introduction\n\n[INFO]\nImportant note here.\n[/INFO]\n\n## Details\n\nSome details." result, err := ToADF([]byte(input)) - require.NoError(t, err) + testutil.RequireNoError(t, err) var doc ADFDocument err = json.Unmarshal([]byte(result), &doc) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should have: extension(toc), heading, panel(info), heading, paragraph - require.Len(t, doc.Content, 5) - assert.Equal(t, "extension", doc.Content[0].Type) - assert.Equal(t, "toc", doc.Content[0].Attrs["extensionKey"]) - assert.Equal(t, "heading", doc.Content[1].Type) - assert.Equal(t, "panel", doc.Content[2].Type) - assert.Equal(t, "info", doc.Content[2].Attrs["panelType"]) - assert.Equal(t, "heading", doc.Content[3].Type) - assert.Equal(t, "paragraph", doc.Content[4].Type) + testutil.Len(t, doc.Content, 5) + testutil.Equal(t, "extension", doc.Content[0].Type) + testutil.Equal(t, "toc", doc.Content[0].Attrs["extensionKey"]) + testutil.Equal(t, "heading", doc.Content[1].Type) + testutil.Equal(t, "panel", doc.Content[2].Type) + testutil.Equal(t, "info", doc.Content[2].Attrs["panelType"]) + testutil.Equal(t, "heading", doc.Content[3].Type) + testutil.Equal(t, "paragraph", doc.Content[4].Type) } func TestToADF_MacroOutputIsValidJSON(t *testing.T) { @@ -783,11 +783,11 @@ func TestToADF_MacroOutputIsValidJSON(t *testing.T) { for _, input := range inputs { result, err := ToADF([]byte(input)) - require.NoError(t, err, "should produce valid ADF for: %s", input) + testutil.RequireNoError(t, err) var parsed map[string]interface{} err = json.Unmarshal([]byte(result), &parsed) - require.NoError(t, err, "output should be valid JSON for: %s", input) - assert.Equal(t, "doc", parsed["type"]) + testutil.RequireNoError(t, err) + testutil.Equal(t, "doc", parsed["type"]) } } diff --git a/tools/cfl/pkg/md/tokenizer_bracket_test.go b/tools/cfl/pkg/md/tokenizer_bracket_test.go index e09bfaf..c13ac72 100644 --- a/tools/cfl/pkg/md/tokenizer_bracket_test.go +++ b/tools/cfl/pkg/md/tokenizer_bracket_test.go @@ -3,22 +3,21 @@ package md import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestTokenizeBrackets_EmptyInput(t *testing.T) { tokens, err := TokenizeBrackets("") - require.NoError(t, err) - assert.Empty(t, tokens) + testutil.RequireNoError(t, err) + testutil.Empty(t, tokens) } func TestTokenizeBrackets_PlainText(t *testing.T) { tokens, err := TokenizeBrackets("Hello world") - require.NoError(t, err) - require.Len(t, tokens, 1) - assert.Equal(t, BracketTokenText, tokens[0].Type) - assert.Equal(t, "Hello world", tokens[0].Text) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 1) + testutil.Equal(t, BracketTokenText, tokens[0].Type) + testutil.Equal(t, "Hello world", tokens[0].Text) } func TestTokenizeBrackets_SimpleMacro(t *testing.T) { @@ -44,10 +43,10 @@ func TestTokenizeBrackets_SimpleMacro(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tokens, err := TokenizeBrackets(tt.input) - require.NoError(t, err) - require.Len(t, tokens, tt.wantCount) - assert.Equal(t, tt.wantType, tokens[0].Type) - assert.Equal(t, tt.wantName, tokens[0].MacroName) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, tt.wantCount) + testutil.Equal(t, tt.wantType, tokens[0].Type) + testutil.Equal(t, tt.wantName, tokens[0].MacroName) }) } } @@ -93,10 +92,10 @@ func TestTokenizeBrackets_WithParameters(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tokens, err := TokenizeBrackets(tt.input) - require.NoError(t, err) - require.Len(t, tokens, 1) - assert.Equal(t, BracketTokenOpenTag, tokens[0].Type) - assert.Equal(t, tt.wantParams, tokens[0].Parameters) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 1) + testutil.Equal(t, BracketTokenOpenTag, tokens[0].Type) + testutil.Equal(t, tt.wantParams, tokens[0].Parameters) }) } } @@ -104,93 +103,93 @@ func TestTokenizeBrackets_WithParameters(t *testing.T) { func TestTokenizeBrackets_OpenAndClose(t *testing.T) { input := "[INFO]content[/INFO]" tokens, err := TokenizeBrackets(input) - require.NoError(t, err) - require.Len(t, tokens, 3) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 3) - assert.Equal(t, BracketTokenOpenTag, tokens[0].Type) - assert.Equal(t, "INFO", tokens[0].MacroName) + testutil.Equal(t, BracketTokenOpenTag, tokens[0].Type) + testutil.Equal(t, "INFO", tokens[0].MacroName) - assert.Equal(t, BracketTokenText, tokens[1].Type) - assert.Equal(t, "content", tokens[1].Text) + testutil.Equal(t, BracketTokenText, tokens[1].Type) + testutil.Equal(t, "content", tokens[1].Text) - assert.Equal(t, BracketTokenCloseTag, tokens[2].Type) - assert.Equal(t, "INFO", tokens[2].MacroName) + testutil.Equal(t, BracketTokenCloseTag, tokens[2].Type) + testutil.Equal(t, "INFO", tokens[2].MacroName) } func TestTokenizeBrackets_WithSurroundingText(t *testing.T) { input := "Before [TOC] after" tokens, err := TokenizeBrackets(input) - require.NoError(t, err) - require.Len(t, tokens, 3) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 3) - assert.Equal(t, BracketTokenText, tokens[0].Type) - assert.Equal(t, "Before ", tokens[0].Text) + testutil.Equal(t, BracketTokenText, tokens[0].Type) + testutil.Equal(t, "Before ", tokens[0].Text) - assert.Equal(t, BracketTokenOpenTag, tokens[1].Type) - assert.Equal(t, "TOC", tokens[1].MacroName) + testutil.Equal(t, BracketTokenOpenTag, tokens[1].Type) + testutil.Equal(t, "TOC", tokens[1].MacroName) - assert.Equal(t, BracketTokenText, tokens[2].Type) - assert.Equal(t, " after", tokens[2].Text) + testutil.Equal(t, BracketTokenText, tokens[2].Type) + testutil.Equal(t, " after", tokens[2].Text) } func TestTokenizeBrackets_NestedMacros(t *testing.T) { input := "[INFO]outer [TOC] content[/INFO]" tokens, err := TokenizeBrackets(input) - require.NoError(t, err) - require.Len(t, tokens, 5) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 5) - assert.Equal(t, BracketTokenOpenTag, tokens[0].Type) - assert.Equal(t, "INFO", tokens[0].MacroName) + testutil.Equal(t, BracketTokenOpenTag, tokens[0].Type) + testutil.Equal(t, "INFO", tokens[0].MacroName) - assert.Equal(t, BracketTokenText, tokens[1].Type) - assert.Equal(t, "outer ", tokens[1].Text) + testutil.Equal(t, BracketTokenText, tokens[1].Type) + testutil.Equal(t, "outer ", tokens[1].Text) - assert.Equal(t, BracketTokenOpenTag, tokens[2].Type) - assert.Equal(t, "TOC", tokens[2].MacroName) + testutil.Equal(t, BracketTokenOpenTag, tokens[2].Type) + testutil.Equal(t, "TOC", tokens[2].MacroName) - assert.Equal(t, BracketTokenText, tokens[3].Type) - assert.Equal(t, " content", tokens[3].Text) + testutil.Equal(t, BracketTokenText, tokens[3].Type) + testutil.Equal(t, " content", tokens[3].Text) - assert.Equal(t, BracketTokenCloseTag, tokens[4].Type) - assert.Equal(t, "INFO", tokens[4].MacroName) + testutil.Equal(t, BracketTokenCloseTag, tokens[4].Type) + testutil.Equal(t, "INFO", tokens[4].MacroName) } func TestTokenizeBrackets_MultipleMacros(t *testing.T) { input := "[INFO]first[/INFO]\n[WARNING]second[/WARNING]" tokens, err := TokenizeBrackets(input) - require.NoError(t, err) - require.Len(t, tokens, 7) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 7) // First macro - assert.Equal(t, BracketTokenOpenTag, tokens[0].Type) - assert.Equal(t, "INFO", tokens[0].MacroName) - assert.Equal(t, BracketTokenText, tokens[1].Type) - assert.Equal(t, "first", tokens[1].Text) - assert.Equal(t, BracketTokenCloseTag, tokens[2].Type) - assert.Equal(t, "INFO", tokens[2].MacroName) + testutil.Equal(t, BracketTokenOpenTag, tokens[0].Type) + testutil.Equal(t, "INFO", tokens[0].MacroName) + testutil.Equal(t, BracketTokenText, tokens[1].Type) + testutil.Equal(t, "first", tokens[1].Text) + testutil.Equal(t, BracketTokenCloseTag, tokens[2].Type) + testutil.Equal(t, "INFO", tokens[2].MacroName) // Text between - assert.Equal(t, BracketTokenText, tokens[3].Type) - assert.Equal(t, "\n", tokens[3].Text) + testutil.Equal(t, BracketTokenText, tokens[3].Type) + testutil.Equal(t, "\n", tokens[3].Text) // Second macro - assert.Equal(t, BracketTokenOpenTag, tokens[4].Type) - assert.Equal(t, "WARNING", tokens[4].MacroName) - assert.Equal(t, BracketTokenText, tokens[5].Type) - assert.Equal(t, "second", tokens[5].Text) - assert.Equal(t, BracketTokenCloseTag, tokens[6].Type) - assert.Equal(t, "WARNING", tokens[6].MacroName) + testutil.Equal(t, BracketTokenOpenTag, tokens[4].Type) + testutil.Equal(t, "WARNING", tokens[4].MacroName) + testutil.Equal(t, BracketTokenText, tokens[5].Type) + testutil.Equal(t, "second", tokens[5].Text) + testutil.Equal(t, BracketTokenCloseTag, tokens[6].Type) + testutil.Equal(t, "WARNING", tokens[6].MacroName) } func TestTokenizeBrackets_Positions(t *testing.T) { input := "abc[TOC]def" tokens, err := TokenizeBrackets(input) - require.NoError(t, err) - require.Len(t, tokens, 3) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 3) - assert.Equal(t, 0, tokens[0].Position) // "abc" - assert.Equal(t, 3, tokens[1].Position) // "[TOC]" - assert.Equal(t, 8, tokens[2].Position) // "def" + testutil.Equal(t, 0, tokens[0].Position) // "abc" + testutil.Equal(t, 3, tokens[1].Position) // "[TOC]" + testutil.Equal(t, 8, tokens[2].Position) // "def" } func TestTokenizeBrackets_MalformedSyntax(t *testing.T) { @@ -224,7 +223,7 @@ func TestTokenizeBrackets_MalformedSyntax(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tokens, err := TokenizeBrackets(tt.input) - require.NoError(t, err, "should not error on malformed input") + testutil.RequireNoError(t, err) // Malformed macro syntax should be treated as text for _, tok := range tokens { if tok.Type == BracketTokenOpenTag || tok.Type == BracketTokenCloseTag { @@ -241,12 +240,12 @@ func TestTokenizeBrackets_MalformedSyntax(t *testing.T) { func TestTokenizeBrackets_BracketsInQuotedValues(t *testing.T) { input := `[INFO title="[Important]"]content[/INFO]` tokens, err := TokenizeBrackets(input) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Should have: open tag, text, close tag - require.Len(t, tokens, 3) - assert.Equal(t, BracketTokenOpenTag, tokens[0].Type) - assert.Equal(t, "[Important]", tokens[0].Parameters["title"]) + testutil.Len(t, tokens, 3) + testutil.Equal(t, BracketTokenOpenTag, tokens[0].Type) + testutil.Equal(t, "[Important]", tokens[0].Parameters["title"]) } func TestTokenizeBrackets_EscapedQuotes(t *testing.T) { @@ -275,8 +274,8 @@ func TestTokenizeBrackets_EscapedQuotes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tokens, err := TokenizeBrackets(tt.input) - require.NoError(t, err) - require.Len(t, tokens, 1) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 1) // Escaped quotes should be unescaped in the returned value var actual string if val, ok := tokens[0].Parameters["title"]; ok { @@ -284,7 +283,7 @@ func TestTokenizeBrackets_EscapedQuotes(t *testing.T) { } else if val, ok := tokens[0].Parameters["msg"]; ok { actual = val } - assert.Equal(t, tt.expected, actual, "parameter value should have unescaped quotes") + testutil.Equal(t, tt.expected, actual) }) } } @@ -296,13 +295,13 @@ multiline content [/INFO]` tokens, err := TokenizeBrackets(input) - require.NoError(t, err) - require.Len(t, tokens, 3) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 3) - assert.Equal(t, BracketTokenOpenTag, tokens[0].Type) - assert.Equal(t, BracketTokenText, tokens[1].Type) - assert.Contains(t, tokens[1].Text, "\n") - assert.Equal(t, BracketTokenCloseTag, tokens[2].Type) + testutil.Equal(t, BracketTokenOpenTag, tokens[0].Type) + testutil.Equal(t, BracketTokenText, tokens[1].Type) + testutil.Contains(t, tokens[1].Text, "\n") + testutil.Equal(t, BracketTokenCloseTag, tokens[2].Type) } func TestTokenizeBrackets_SelfClosing(t *testing.T) { @@ -346,11 +345,11 @@ func TestTokenizeBrackets_SelfClosing(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tokens, err := TokenizeBrackets(tt.input) - require.NoError(t, err) - require.Len(t, tokens, tt.wantCount) - assert.Equal(t, tt.wantType, tokens[0].Type) - assert.Equal(t, "TOC", tokens[0].MacroName) - assert.Equal(t, tt.wantParams, tokens[0].Parameters) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, tt.wantCount) + testutil.Equal(t, tt.wantType, tokens[0].Type) + testutil.Equal(t, "TOC", tokens[0].MacroName) + testutil.Equal(t, tt.wantParams, tokens[0].Parameters) }) } } @@ -358,7 +357,7 @@ func TestTokenizeBrackets_SelfClosing(t *testing.T) { func TestTokenizeBrackets_DeeplyNested(t *testing.T) { input := "[INFO][WARNING][NOTE]deep[/NOTE][/WARNING][/INFO]" tokens, err := TokenizeBrackets(input) - require.NoError(t, err) + testutil.RequireNoError(t, err) // Count token types openCount := 0 @@ -375,20 +374,20 @@ func TestTokenizeBrackets_DeeplyNested(t *testing.T) { } } - assert.Equal(t, 3, openCount, "should have 3 open tags") - assert.Equal(t, 3, closeCount, "should have 3 close tags") - assert.Equal(t, 1, textCount, "should have 1 text token") + testutil.Equal(t, 3, openCount) + testutil.Equal(t, 3, closeCount) + testutil.Equal(t, 1, textCount) } func TestTokenizeBrackets_SpecialCharactersInBody(t *testing.T) { input := "[INFO] & < > \"[/INFO]" tokens, err := TokenizeBrackets(input) - require.NoError(t, err) - require.Len(t, tokens, 3) + testutil.RequireNoError(t, err) + testutil.Len(t, tokens, 3) - assert.Equal(t, BracketTokenText, tokens[1].Type) - assert.Contains(t, tokens[1].Text, " & < > \"[/INFO]" tokens, err := TokenizeBrackets(input) testutil.RequireNoError(t, err) @@ -393,6 +414,7 @@ func TestTokenizeBrackets_SpecialCharactersInBody(t *testing.T) { } func TestTokenizeBrackets_WhitespaceHandling(t *testing.T) { + t.Parallel() tests := []struct { name string input string @@ -427,6 +449,7 @@ func TestTokenizeBrackets_WhitespaceHandling(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() tokens, err := TokenizeBrackets(tt.input) testutil.RequireNoError(t, err) testutil.GreaterOrEqual(t, len(tokens), 1) diff --git a/tools/cfl/pkg/md/tokenizer_xml_test.go b/tools/cfl/pkg/md/tokenizer_xml_test.go index 90c1746..25cea19 100644 --- a/tools/cfl/pkg/md/tokenizer_xml_test.go +++ b/tools/cfl/pkg/md/tokenizer_xml_test.go @@ -8,12 +8,14 @@ import ( ) func TestTokenizeConfluenceXML_EmptyInput(t *testing.T) { + t.Parallel() tokens, err := TokenizeConfluenceXML("") testutil.RequireNoError(t, err) testutil.Empty(t, tokens) } func TestTokenizeConfluenceXML_PlainHTML(t *testing.T) { + t.Parallel() input := "

    Hello world

    " tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -23,6 +25,7 @@ func TestTokenizeConfluenceXML_PlainHTML(t *testing.T) { } func TestTokenizeConfluenceXML_SimpleMacro(t *testing.T) { + t.Parallel() input := `` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -35,6 +38,7 @@ func TestTokenizeConfluenceXML_SimpleMacro(t *testing.T) { } func TestTokenizeConfluenceXML_MacroWithParameter(t *testing.T) { + t.Parallel() input := `3` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -51,6 +55,7 @@ func TestTokenizeConfluenceXML_MacroWithParameter(t *testing.T) { } func TestTokenizeConfluenceXML_MacroWithMultipleParameters(t *testing.T) { + t.Parallel() input := `31flat` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -72,6 +77,7 @@ func TestTokenizeConfluenceXML_MacroWithMultipleParameters(t *testing.T) { } func TestTokenizeConfluenceXML_PanelWithRichTextBody(t *testing.T) { + t.Parallel() input := `

    Content

    ` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -95,6 +101,7 @@ func TestTokenizeConfluenceXML_PanelWithRichTextBody(t *testing.T) { } func TestTokenizeConfluenceXML_PanelWithTitleAndBody(t *testing.T) { + t.Parallel() input := `Watch Out

    Warning content

    ` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -110,6 +117,7 @@ func TestTokenizeConfluenceXML_PanelWithTitleAndBody(t *testing.T) { } func TestTokenizeConfluenceXML_CodeMacroWithCDATA(t *testing.T) { + t.Parallel() input := `python` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -132,6 +140,7 @@ func TestTokenizeConfluenceXML_CodeMacroWithCDATA(t *testing.T) { } func TestTokenizeConfluenceXML_NestedMacros(t *testing.T) { + t.Parallel() input := `

    Before

    After

    ` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -153,6 +162,7 @@ func TestTokenizeConfluenceXML_NestedMacros(t *testing.T) { } func TestTokenizeConfluenceXML_WithSurroundingHTML(t *testing.T) { + t.Parallel() input := `

    Title

    Content

    ` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -173,10 +183,12 @@ func TestTokenizeConfluenceXML_WithSurroundingHTML(t *testing.T) { } func TestTokenizeConfluenceXML_AllPanelTypes(t *testing.T) { + t.Parallel() panelTypes := []string{"info", "warning", "note", "tip", "expand"} for _, pt := range panelTypes { t.Run(pt, func(t *testing.T) { + t.Parallel() input := `

    Content

    ` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -187,6 +199,7 @@ func TestTokenizeConfluenceXML_AllPanelTypes(t *testing.T) { } func TestTokenizeConfluenceXML_Positions(t *testing.T) { + t.Parallel() input := `abcdef` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -198,6 +211,7 @@ func TestTokenizeConfluenceXML_Positions(t *testing.T) { } func TestTokenizeConfluenceXML_CDATAWithSpecialChars(t *testing.T) { + t.Parallel() input := ` 5 { fmt.Println("test") }]]>` @@ -220,6 +234,7 @@ func TestTokenizeConfluenceXML_CDATAWithSpecialChars(t *testing.T) { } func TestTokenizeConfluenceXML_MultilineCDATA(t *testing.T) { + t.Parallel() input := `

    Deep

    ` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -268,6 +284,7 @@ func TestTokenizeConfluenceXML_DeeplyNestedMacros(t *testing.T) { } func TestTokenizeConfluenceXML_WhitespaceInMacro(t *testing.T) { + t.Parallel() input := ` 3 ` @@ -286,6 +303,7 @@ func TestTokenizeConfluenceXML_WhitespaceInMacro(t *testing.T) { } func TestTokenizeConfluenceXML_EmptyParameter(t *testing.T) { + t.Parallel() input := `` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -301,6 +319,7 @@ func TestTokenizeConfluenceXML_EmptyParameter(t *testing.T) { } func TestTokenizeConfluenceXML_EmptyRichTextBody(t *testing.T) { + t.Parallel() input := `` tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) @@ -321,6 +340,7 @@ func TestTokenizeConfluenceXML_EmptyRichTextBody(t *testing.T) { } func TestTokenizeConfluenceXML_MacroNameCaseInsensitive(t *testing.T) { + t.Parallel() inputs := []string{ ``, ``, @@ -329,6 +349,7 @@ func TestTokenizeConfluenceXML_MacroNameCaseInsensitive(t *testing.T) { for _, input := range inputs { t.Run(input, func(t *testing.T) { + t.Parallel() tokens, err := TokenizeConfluenceXML(input) testutil.RequireNoError(t, err) testutil.GreaterOrEqual(t, len(tokens), 1) @@ -339,6 +360,7 @@ func TestTokenizeConfluenceXML_MacroNameCaseInsensitive(t *testing.T) { } func TestExtractCDATAContent(t *testing.T) { + t.Parallel() tests := []struct { input string expected string @@ -352,6 +374,7 @@ func TestExtractCDATAContent(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { + t.Parallel() result := ExtractCDATAContent(tt.input) testutil.Equal(t, tt.expected, result) }) @@ -360,6 +383,7 @@ func TestExtractCDATAContent(t *testing.T) { // Tests for self-closing macros (issue #56) func TestTokenizeConfluenceXML_SelfClosingMacro(t *testing.T) { + t.Parallel() tests := []struct { name string input string @@ -399,6 +423,7 @@ func TestTokenizeConfluenceXML_SelfClosingMacro(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() tokens, err := TokenizeConfluenceXML(tt.input) testutil.RequireNoError(t, err) @@ -425,6 +450,7 @@ func TestTokenizeConfluenceXML_SelfClosingMacro(t *testing.T) { } func TestTokenizeConfluenceXML_SelfClosingNestedInBodyMacro(t *testing.T) { + t.Parallel() // This is the exact scenario from issue #56 input := `

    ` @@ -473,6 +499,7 @@ func TestTokenizeConfluenceXML_SelfClosingNestedInBodyMacro(t *testing.T) { } func TestTokenizeConfluenceXML_SelfClosingVsRegularMacro(t *testing.T) { + t.Parallel() // Make sure regular macros still work and are distinguished from self-closing regular := `` selfClosing := `` diff --git a/tools/cfl/pkg/md/wikilink_test.go b/tools/cfl/pkg/md/wikilink_test.go index 1df8bc9..e6fbc1f 100644 --- a/tools/cfl/pkg/md/wikilink_test.go +++ b/tools/cfl/pkg/md/wikilink_test.go @@ -9,6 +9,7 @@ import ( ) func TestParseWikiLink(t *testing.T) { + t.Parallel() tests := []struct { name string input string @@ -58,6 +59,7 @@ func TestParseWikiLink(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result := ParseWikiLink(tt.input) testutil.Equal(t, tt.expected, result) }) @@ -65,6 +67,7 @@ func TestParseWikiLink(t *testing.T) { } func TestIsSpaceKey(t *testing.T) { + t.Parallel() tests := []struct { input string expected bool @@ -85,12 +88,14 @@ func TestIsSpaceKey(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { + t.Parallel() testutil.Equal(t, tt.expected, isSpaceKey(tt.input)) }) } } func TestRenderWikiLinkToStorage(t *testing.T) { + t.Parallel() tests := []struct { name string wl WikiLink @@ -118,6 +123,7 @@ func TestRenderWikiLinkToStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result := RenderWikiLinkToStorage(tt.wl) testutil.Equal(t, tt.expected, result) }) @@ -125,6 +131,7 @@ func TestRenderWikiLinkToStorage(t *testing.T) { } func TestRenderWikiLinkToBracket(t *testing.T) { + t.Parallel() tests := []struct { name string wl WikiLink @@ -144,6 +151,7 @@ func TestRenderWikiLinkToBracket(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result := RenderWikiLinkToBracket(tt.wl) testutil.Equal(t, tt.expected, result) }) @@ -151,6 +159,7 @@ func TestRenderWikiLinkToBracket(t *testing.T) { } func TestPreprocessWikiLinks(t *testing.T) { + t.Parallel() tests := []struct { name string input string @@ -197,6 +206,7 @@ func TestPreprocessWikiLinks(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() output, links := preprocessWikiLinks([]byte(tt.input)) testutil.Equal(t, tt.expectedLinks, len(links)) tt.checkOutput(t, string(output), links) @@ -205,6 +215,7 @@ func TestPreprocessWikiLinks(t *testing.T) { } func TestToConfluenceStorage_WikiLinks(t *testing.T) { + t.Parallel() tests := []struct { name string markdown string @@ -248,6 +259,7 @@ func TestToConfluenceStorage_WikiLinks(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result, err := ToConfluenceStorage([]byte(tt.markdown)) testutil.RequireNoError(t, err) for _, s := range tt.contains { @@ -258,6 +270,7 @@ func TestToConfluenceStorage_WikiLinks(t *testing.T) { } func TestToADF_WikiLinks(t *testing.T) { + t.Parallel() tests := []struct { name string markdown string @@ -292,6 +305,7 @@ func TestToADF_WikiLinks(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result, err := ToADF([]byte(tt.markdown)) testutil.RequireNoError(t, err) @@ -305,6 +319,7 @@ func TestToADF_WikiLinks(t *testing.T) { } func TestConvertACLinksToPlaceholders(t *testing.T) { + t.Parallel() tests := []struct { name string html string @@ -361,6 +376,7 @@ func TestConvertACLinksToPlaceholders(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() output, links := convertACLinksToPlaceholders(tt.html) testutil.Equal(t, tt.expectedLinks, len(links)) tt.checkOutput(t, output, links) @@ -369,6 +385,7 @@ func TestConvertACLinksToPlaceholders(t *testing.T) { } func TestConvertACLinksToMarkdownLinks(t *testing.T) { + t.Parallel() tests := []struct { name string html string @@ -401,6 +418,7 @@ func TestConvertACLinksToMarkdownLinks(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result := convertACLinksToMarkdownLinks(tt.html) testutil.Equal(t, tt.expected, result) }) @@ -408,6 +426,7 @@ func TestConvertACLinksToMarkdownLinks(t *testing.T) { } func TestRoundtrip_WikiLinks_Storage(t *testing.T) { + t.Parallel() // Test: markdown with wiki links → storage → markdown with wiki links input := "See [[My Page]] and [[DEV:Architecture]] for details." @@ -426,6 +445,7 @@ func TestRoundtrip_WikiLinks_Storage(t *testing.T) { } func TestRoundtrip_WikiLinks_WithMacros(t *testing.T) { + t.Parallel() // Wiki links + macros should both survive full roundtrip input := "[TOC]\n\nSee [[My Page]] for details.\n\n[INFO]\nImportant info about [[DEV:Architecture]]\n[/INFO]" @@ -452,6 +472,7 @@ func TestRoundtrip_WikiLinks_WithMacros(t *testing.T) { } func TestFromConfluenceStorage_WikiLinks_Default(t *testing.T) { + t.Parallel() // Without --show-macros, ac:link should become plain text link html := `

    See ` + ` for details.

    ` @@ -465,6 +486,7 @@ func TestFromConfluenceStorage_WikiLinks_Default(t *testing.T) { } func TestFromConfluenceStorage_WikiLinks_ShowMacros(t *testing.T) { + t.Parallel() html := `

    See ` + ` for details.

    ` @@ -474,6 +496,7 @@ func TestFromConfluenceStorage_WikiLinks_ShowMacros(t *testing.T) { } func TestPreprocessWikiLinksForADF(t *testing.T) { + t.Parallel() tests := []struct { name string input string @@ -503,6 +526,7 @@ func TestPreprocessWikiLinksForADF(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result := preprocessWikiLinksForADF([]byte(tt.input)) testutil.Equal(t, tt.expected, string(result)) }) @@ -510,6 +534,7 @@ func TestPreprocessWikiLinksForADF(t *testing.T) { } func TestPreprocessWikiLinks_CodeBlockProtection(t *testing.T) { + t.Parallel() tests := []struct { name string input string @@ -555,6 +580,7 @@ func TestPreprocessWikiLinks_CodeBlockProtection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() output, links := preprocessWikiLinks([]byte(tt.input)) testutil.Equal(t, tt.expectedLinks, len(links)) tt.checkOutput(t, string(output), links) @@ -563,6 +589,7 @@ func TestPreprocessWikiLinks_CodeBlockProtection(t *testing.T) { } func TestToConfluenceStorage_WikiLinksInCodeBlock(t *testing.T) { + t.Parallel() tests := []struct { name string markdown string @@ -602,6 +629,7 @@ func TestToConfluenceStorage_WikiLinksInCodeBlock(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result, err := ToConfluenceStorage([]byte(tt.markdown)) testutil.RequireNoError(t, err) for _, s := range tt.contains { @@ -615,6 +643,7 @@ func TestToConfluenceStorage_WikiLinksInCodeBlock(t *testing.T) { } func TestToADF_WikiLinksInCodeBlock(t *testing.T) { + t.Parallel() tests := []struct { name string markdown string @@ -649,6 +678,7 @@ func TestToADF_WikiLinksInCodeBlock(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result, err := ToADF([]byte(tt.markdown)) testutil.RequireNoError(t, err) @@ -661,6 +691,7 @@ func TestToADF_WikiLinksInCodeBlock(t *testing.T) { } func TestPreprocessWikiLinksForADF_CodeBlockProtection(t *testing.T) { + t.Parallel() tests := []struct { name string input string @@ -685,6 +716,7 @@ func TestPreprocessWikiLinksForADF_CodeBlockProtection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result := preprocessWikiLinksForADF([]byte(tt.input)) testutil.Equal(t, tt.expected, string(result)) }) @@ -692,6 +724,7 @@ func TestPreprocessWikiLinksForADF_CodeBlockProtection(t *testing.T) { } func TestWikiLink_EscapedTitleInStorage(t *testing.T) { + t.Parallel() // Titles with XML-special characters should be properly escaped in storage format wl := WikiLink{Title: `Page & "Stuff" `} storage := RenderWikiLinkToStorage(wl) @@ -701,6 +734,7 @@ func TestWikiLink_EscapedTitleInStorage(t *testing.T) { } func TestWikiLink_NotConfusedWithMarkdownLinks(t *testing.T) { + t.Parallel() // Standard markdown links should not be affected input := "[regular link](https://example.com)" output, links := preprocessWikiLinks([]byte(input)) @@ -709,6 +743,7 @@ func TestWikiLink_NotConfusedWithMarkdownLinks(t *testing.T) { } func TestWikiLink_NotConfusedWithBracketMacros(t *testing.T) { + t.Parallel() // Bracket macros use single brackets and should not be confused with wiki-links input := "[TOC]\n\n[[My Page]]" output, links := preprocessWikiLinks([]byte(input)) @@ -718,6 +753,7 @@ func TestWikiLink_NotConfusedWithBracketMacros(t *testing.T) { } func TestMultipleWikiLinksInLine(t *testing.T) { + t.Parallel() input := "Compare [[Page A]], [[Page B]], and [[DEV:Page C]]." storage, err := ToConfluenceStorage([]byte(input)) testutil.RequireNoError(t, err) @@ -731,6 +767,7 @@ func TestMultipleWikiLinksInLine(t *testing.T) { } func TestToADF_WikiLink_SpecialCharsInTitle(t *testing.T) { + t.Parallel() // Titles with special characters should be properly URL-encoded in ADF path tests := []struct { name string @@ -757,6 +794,7 @@ func TestToADF_WikiLink_SpecialCharsInTitle(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() result, err := ToADF([]byte(tt.markdown)) testutil.RequireNoError(t, err) diff --git a/tools/jtk/api/attachments_test.go b/tools/jtk/api/attachments_test.go index 1d65714..133b09b 100644 --- a/tools/jtk/api/attachments_test.go +++ b/tools/jtk/api/attachments_test.go @@ -13,6 +13,7 @@ import ( ) func TestFlexibleID_UnmarshalJSON(t *testing.T) { + t.Parallel() tests := []struct { name string input string @@ -48,6 +49,7 @@ func TestFlexibleID_UnmarshalJSON(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() var id FlexibleID err := json.Unmarshal([]byte(tt.input), &id) if tt.wantErr { @@ -62,6 +64,7 @@ func TestFlexibleID_UnmarshalJSON(t *testing.T) { } func TestGetIssueAttachments(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/issue/PROJ-123") testutil.Equal(t, r.URL.Query().Get("fields"), "attachment") @@ -103,6 +106,7 @@ func TestGetIssueAttachments(t *testing.T) { } func TestGetIssueAttachments_EmptyIssueKey(t *testing.T) { + t.Parallel() client, _ := New(ClientConfig{ URL: "http://unused", Email: "test@example.com", @@ -115,6 +119,7 @@ func TestGetIssueAttachments_EmptyIssueKey(t *testing.T) { } func TestGetAttachment(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/attachment/10001") w.WriteHeader(http.StatusOK) @@ -143,6 +148,7 @@ func TestGetAttachment(t *testing.T) { } func TestGetAttachment_EmptyID(t *testing.T) { + t.Parallel() client, _ := New(ClientConfig{ URL: "http://unused", Email: "test@example.com", @@ -155,6 +161,7 @@ func TestGetAttachment_EmptyID(t *testing.T) { } func TestDeleteAttachment(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodDelete) testutil.Equal(t, r.URL.Path, "/rest/api/3/attachment/10001") @@ -174,6 +181,7 @@ func TestDeleteAttachment(t *testing.T) { } func TestDeleteAttachment_EmptyID(t *testing.T) { + t.Parallel() client, _ := New(ClientConfig{ URL: "http://unused", Email: "test@example.com", @@ -186,6 +194,7 @@ func TestDeleteAttachment_EmptyID(t *testing.T) { } func TestDownloadAttachment(t *testing.T) { + t.Parallel() content := []byte("Test file content") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) @@ -217,6 +226,7 @@ func TestDownloadAttachment(t *testing.T) { } func TestDownloadAttachment_ToDirectory(t *testing.T) { + t.Parallel() content := []byte("Test file content") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) @@ -248,6 +258,7 @@ func TestDownloadAttachment_ToDirectory(t *testing.T) { } func TestDownloadAttachment_NilAttachment(t *testing.T) { + t.Parallel() client, _ := New(ClientConfig{ URL: "http://unused", Email: "test@example.com", @@ -260,6 +271,7 @@ func TestDownloadAttachment_NilAttachment(t *testing.T) { } func TestDownloadAttachment_NoContentURL(t *testing.T) { + t.Parallel() client, _ := New(ClientConfig{ URL: "http://unused", Email: "test@example.com", @@ -273,6 +285,7 @@ func TestDownloadAttachment_NoContentURL(t *testing.T) { } func TestFormatFileSize(t *testing.T) { + t.Parallel() tests := []struct { bytes int64 expected string @@ -288,6 +301,7 @@ func TestFormatFileSize(t *testing.T) { for _, tt := range tests { t.Run(tt.expected, func(t *testing.T) { + t.Parallel() result := FormatFileSize(tt.bytes) testutil.Equal(t, result, tt.expected) }) diff --git a/tools/jtk/api/automation.go b/tools/jtk/api/automation.go index 6d15476..ba61b3e 100644 --- a/tools/jtk/api/automation.go +++ b/tools/jtk/api/automation.go @@ -11,7 +11,7 @@ import ( func (c *Client) ListAutomationRules(ctx context.Context) ([]AutomationRuleSummary, error) { base, err := c.AutomationBaseURL(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("listing automation rules: %w", err) } var all []AutomationRuleSummary @@ -44,7 +44,7 @@ func (c *Client) ListAutomationRules(ctx context.Context) ([]AutomationRuleSumma func (c *Client) ListAutomationRulesFiltered(ctx context.Context, state string) ([]AutomationRuleSummary, error) { rules, err := c.ListAutomationRules(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("listing automation rules (filtered): %w", err) } if state == "" { @@ -64,7 +64,7 @@ func (c *Client) ListAutomationRulesFiltered(ctx context.Context, state string) func (c *Client) GetAutomationRule(ctx context.Context, ruleID string) (*AutomationRule, error) { base, err := c.AutomationBaseURL(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("getting automation rule %s: %w", ruleID, err) } urlStr := fmt.Sprintf("%s/rule/%s", base, url.PathEscape(ruleID)) @@ -102,7 +102,7 @@ func (c *Client) GetAutomationRule(ctx context.Context, ruleID string) (*Automat func (c *Client) GetAutomationRuleRaw(ctx context.Context, ruleID string) ([]byte, error) { base, err := c.AutomationBaseURL(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("getting automation rule %s (raw): %w", ruleID, err) } urlStr := fmt.Sprintf("%s/rule/%s", base, url.PathEscape(ruleID)) @@ -120,7 +120,7 @@ func (c *Client) GetAutomationRuleRaw(ctx context.Context, ruleID string) ([]byt func (c *Client) UpdateAutomationRule(ctx context.Context, ruleID string, ruleJSON json.RawMessage) error { base, err := c.AutomationBaseURL(ctx) if err != nil { - return err + return fmt.Errorf("updating automation rule %s: %w", ruleID, err) } urlStr := fmt.Sprintf("%s/rule/%s", base, url.PathEscape(ruleID)) @@ -138,7 +138,7 @@ func (c *Client) UpdateAutomationRule(ctx context.Context, ruleID string, ruleJS func (c *Client) CreateAutomationRule(ctx context.Context, ruleJSON json.RawMessage) (json.RawMessage, error) { base, err := c.AutomationBaseURL(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("creating automation rule: %w", err) } urlStr := fmt.Sprintf("%s/rule", base) @@ -154,7 +154,7 @@ func (c *Client) CreateAutomationRule(ctx context.Context, ruleJSON json.RawMess func (c *Client) SetAutomationRuleState(ctx context.Context, ruleID string, enabled bool) error { base, err := c.AutomationBaseURL(ctx) if err != nil { - return err + return fmt.Errorf("setting automation rule %s state: %w", ruleID, err) } state := "DISABLED" diff --git a/tools/jtk/api/automation_test.go b/tools/jtk/api/automation_test.go index 3e603f9..84e3e31 100644 --- a/tools/jtk/api/automation_test.go +++ b/tools/jtk/api/automation_test.go @@ -25,7 +25,9 @@ func newTestClientWithServer(t *testing.T, handler http.HandlerFunc) (*Client, * } func TestGetCloudID(t *testing.T) { + t.Parallel() t.Run("successful fetch", func(t *testing.T) { + t.Parallel() client, server := newTestClientWithServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/_edge/tenant_info" { w.WriteHeader(http.StatusOK) @@ -47,6 +49,7 @@ func TestGetCloudID(t *testing.T) { }) t.Run("empty cloud ID", func(t *testing.T) { + t.Parallel() client, server := newTestClientWithServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"cloudId":""}`)) diff --git a/tools/jtk/api/boards.go b/tools/jtk/api/boards.go index 314019f..675cc56 100644 --- a/tools/jtk/api/boards.go +++ b/tools/jtk/api/boards.go @@ -24,7 +24,7 @@ func (c *Client) ListBoards(ctx context.Context, projectKeyOrID string, startAt, urlStr := buildURL(fmt.Sprintf("%s/board", c.AgileURL), params) body, err := c.Get(ctx, urlStr) if err != nil { - return nil, err + return nil, fmt.Errorf("listing boards: %w", err) } var result BoardsResponse @@ -40,7 +40,7 @@ func (c *Client) GetBoard(ctx context.Context, boardID int) (*Board, error) { urlStr := fmt.Sprintf("%s/board/%d", c.AgileURL, boardID) body, err := c.Get(ctx, urlStr) if err != nil { - return nil, err + return nil, fmt.Errorf("getting board %d: %w", boardID, err) } var board Board diff --git a/tools/jtk/api/client_test.go b/tools/jtk/api/client_test.go index d1d5910..07b17ca 100644 --- a/tools/jtk/api/client_test.go +++ b/tools/jtk/api/client_test.go @@ -12,6 +12,7 @@ import ( ) func TestNew(t *testing.T) { + t.Parallel() tests := []struct { name string cfg ClientConfig diff --git a/tools/jtk/api/errors_test.go b/tools/jtk/api/errors_test.go index 2ac62db..c882979 100644 --- a/tools/jtk/api/errors_test.go +++ b/tools/jtk/api/errors_test.go @@ -11,6 +11,7 @@ import ( ) func TestAPIError_Error(t *testing.T) { + t.Parallel() tests := []struct { name string apiErr *APIError @@ -56,6 +57,7 @@ func TestAPIError_Error(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() got := tt.apiErr.Error() testutil.Equal(t, got, tt.want) }) @@ -63,6 +65,7 @@ func TestAPIError_Error(t *testing.T) { } func TestParseAPIError(t *testing.T) { + t.Parallel() tests := []struct { name string statusCode int @@ -130,6 +133,7 @@ func TestParseAPIError(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() // Use shared ParseAPIError which takes (statusCode, body) err := sharederrors.ParseAPIError(tt.statusCode, []byte(tt.body)) testutil.True(t, errors.Is(err, tt.wantErr), fmt.Sprintf("expected %v, got %v", tt.wantErr, err)) @@ -142,6 +146,7 @@ func TestParseAPIError(t *testing.T) { } func TestParseAPIError_418_NonStandard(t *testing.T) { + t.Parallel() // Test a non-standard status code that isn't explicitly handled body := `{"errorMessages": ["I'm a teapot"]}` @@ -155,6 +160,7 @@ func TestParseAPIError_418_NonStandard(t *testing.T) { } func TestIsNotFound(t *testing.T) { + t.Parallel() testutil.True(t, sharederrors.IsNotFound(sharederrors.ErrNotFound)) testutil.True(t, sharederrors.IsNotFound(fmt.Errorf("wrapped: %w", sharederrors.ErrNotFound))) testutil.False(t, sharederrors.IsNotFound(sharederrors.ErrUnauthorized)) @@ -162,12 +168,14 @@ func TestIsNotFound(t *testing.T) { } func TestIsUnauthorized(t *testing.T) { + t.Parallel() testutil.True(t, sharederrors.IsUnauthorized(sharederrors.ErrUnauthorized)) testutil.False(t, sharederrors.IsUnauthorized(sharederrors.ErrNotFound)) testutil.False(t, sharederrors.IsUnauthorized(nil)) } func TestIsForbidden(t *testing.T) { + t.Parallel() testutil.True(t, sharederrors.IsForbidden(sharederrors.ErrForbidden)) testutil.False(t, sharederrors.IsForbidden(sharederrors.ErrNotFound)) testutil.False(t, sharederrors.IsForbidden(nil)) diff --git a/tools/jtk/api/field_management.go b/tools/jtk/api/field_management.go index 3c73c45..be887ff 100644 --- a/tools/jtk/api/field_management.go +++ b/tools/jtk/api/field_management.go @@ -149,7 +149,7 @@ func (c *Client) GetFieldContexts(ctx context.Context, fieldID string) (*FieldCo func (c *Client) GetDefaultFieldContext(ctx context.Context, fieldID string) (*FieldContext, error) { result, err := c.GetFieldContexts(ctx, fieldID) if err != nil { - return nil, err + return nil, fmt.Errorf("getting default field context for %s: %w", fieldID, err) } if len(result.Values) == 0 { diff --git a/tools/jtk/api/field_management_test.go b/tools/jtk/api/field_management_test.go index c9cabc2..5c20771 100644 --- a/tools/jtk/api/field_management_test.go +++ b/tools/jtk/api/field_management_test.go @@ -26,6 +26,7 @@ func newTestClient(t *testing.T, server *httptest.Server) *Client { } func TestCreateField(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) testutil.Equal(t, r.URL.Path, "/rest/api/3/field") @@ -57,6 +58,7 @@ func TestCreateField(t *testing.T) { } func TestCreateField_ServerError(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"errorMessages":["Field name already exists"]}`)) @@ -69,6 +71,7 @@ func TestCreateField_ServerError(t *testing.T) { } func TestTrashField(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) testutil.Equal(t, r.URL.Path, "/rest/api/3/field/customfield_10100/trash") @@ -82,12 +85,14 @@ func TestTrashField(t *testing.T) { } func TestTrashField_EmptyID(t *testing.T) { + t.Parallel() client := newTestClient(t, nil) err := client.TrashField(context.Background(), "") testutil.True(t, errors.Is(err, ErrFieldIDRequired)) } func TestRestoreField(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) testutil.Equal(t, r.URL.Path, "/rest/api/3/field/customfield_10100/restore") @@ -101,12 +106,14 @@ func TestRestoreField(t *testing.T) { } func TestRestoreField_EmptyID(t *testing.T) { + t.Parallel() client := newTestClient(t, nil) err := client.RestoreField(context.Background(), "") testutil.True(t, errors.Is(err, ErrFieldIDRequired)) } func TestGetFieldContexts(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodGet) testutil.Equal(t, r.URL.Path, "/rest/api/3/field/customfield_10100/context") @@ -131,12 +138,14 @@ func TestGetFieldContexts(t *testing.T) { } func TestGetFieldContexts_EmptyID(t *testing.T) { + t.Parallel() client := newTestClient(t, nil) _, err := client.GetFieldContexts(context.Background(), "") testutil.True(t, errors.Is(err, ErrFieldIDRequired)) } func TestGetDefaultFieldContext(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(FieldContextsResponse{ Values: []FieldContext{ @@ -154,6 +163,7 @@ func TestGetDefaultFieldContext(t *testing.T) { } func TestGetDefaultFieldContext_NoContexts(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(FieldContextsResponse{Values: []FieldContext{}}) })) @@ -166,6 +176,7 @@ func TestGetDefaultFieldContext_NoContexts(t *testing.T) { } func TestCreateFieldContext(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) testutil.Equal(t, r.URL.Path, "/rest/api/3/field/customfield_10100/context") @@ -193,12 +204,14 @@ func TestCreateFieldContext(t *testing.T) { } func TestCreateFieldContext_EmptyID(t *testing.T) { + t.Parallel() client := newTestClient(t, nil) _, err := client.CreateFieldContext(context.Background(), "", &CreateFieldContextRequest{Name: "test"}) testutil.True(t, errors.Is(err, ErrFieldIDRequired)) } func TestDeleteFieldContext(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodDelete) testutil.Equal(t, r.URL.Path, "/rest/api/3/field/customfield_10100/context/10003") @@ -212,12 +225,14 @@ func TestDeleteFieldContext(t *testing.T) { } func TestDeleteFieldContext_EmptyID(t *testing.T) { + t.Parallel() client := newTestClient(t, nil) err := client.DeleteFieldContext(context.Background(), "", "10003") testutil.True(t, errors.Is(err, ErrFieldIDRequired)) } func TestGetFieldContextOptions(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodGet) testutil.Equal(t, r.URL.Path, "/rest/api/3/field/customfield_10100/context/10001/option") @@ -241,12 +256,14 @@ func TestGetFieldContextOptions(t *testing.T) { } func TestGetFieldContextOptions_EmptyID(t *testing.T) { + t.Parallel() client := newTestClient(t, nil) _, err := client.GetFieldContextOptions(context.Background(), "", "10001") testutil.True(t, errors.Is(err, ErrFieldIDRequired)) } func TestCreateFieldContextOptions(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) testutil.Equal(t, r.URL.Path, "/rest/api/3/field/customfield_10100/context/10001/option") @@ -277,12 +294,14 @@ func TestCreateFieldContextOptions(t *testing.T) { } func TestCreateFieldContextOptions_EmptyID(t *testing.T) { + t.Parallel() client := newTestClient(t, nil) _, err := client.CreateFieldContextOptions(context.Background(), "", "10001", &CreateFieldContextOptionsRequest{}) testutil.True(t, errors.Is(err, ErrFieldIDRequired)) } func TestUpdateFieldContextOptions(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPut) testutil.Equal(t, r.URL.Path, "/rest/api/3/field/customfield_10100/context/10001/option") @@ -314,12 +333,14 @@ func TestUpdateFieldContextOptions(t *testing.T) { } func TestUpdateFieldContextOptions_EmptyID(t *testing.T) { + t.Parallel() client := newTestClient(t, nil) _, err := client.UpdateFieldContextOptions(context.Background(), "", "10001", &UpdateFieldContextOptionsRequest{}) testutil.True(t, errors.Is(err, ErrFieldIDRequired)) } func TestDeleteFieldContextOption(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodDelete) testutil.Equal(t, r.URL.Path, "/rest/api/3/field/customfield_10100/context/10001/option/3") @@ -333,6 +354,7 @@ func TestDeleteFieldContextOption(t *testing.T) { } func TestDeleteFieldContextOption_EmptyID(t *testing.T) { + t.Parallel() client := newTestClient(t, nil) err := client.DeleteFieldContextOption(context.Background(), "", "10001", "3") testutil.True(t, errors.Is(err, ErrFieldIDRequired)) diff --git a/tools/jtk/api/fields.go b/tools/jtk/api/fields.go index 30a4c31..135e900 100644 --- a/tools/jtk/api/fields.go +++ b/tools/jtk/api/fields.go @@ -28,7 +28,7 @@ func (c *Client) GetFields(ctx context.Context) ([]Field, error) { func (c *Client) GetCustomFields(ctx context.Context) ([]Field, error) { fields, err := c.GetFields(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("getting custom fields: %w", err) } var customFields []Field @@ -178,7 +178,7 @@ func (c *Client) GetFieldOptions(ctx context.Context, fieldID string) ([]FieldOp func (c *Client) GetFieldOptionsFromEditMeta(ctx context.Context, issueKey, fieldID string) ([]FieldOptionValue, error) { meta, err := c.GetIssueEditMeta(ctx, issueKey) if err != nil { - return nil, err + return nil, fmt.Errorf("getting field options from edit metadata for %s: %w", issueKey, err) } fieldsData, ok := meta["fields"].(map[string]any) diff --git a/tools/jtk/api/fields_test.go b/tools/jtk/api/fields_test.go index b1c3a90..11d6b87 100644 --- a/tools/jtk/api/fields_test.go +++ b/tools/jtk/api/fields_test.go @@ -21,6 +21,7 @@ func getTestFields() []Field { } func TestFindFieldByName(t *testing.T) { + t.Parallel() fields := getTestFields() tests := []struct { diff --git a/tools/jtk/api/markdown_test.go b/tools/jtk/api/markdown_test.go index 8d70dc2..9e2c137 100644 --- a/tools/jtk/api/markdown_test.go +++ b/tools/jtk/api/markdown_test.go @@ -8,11 +8,13 @@ import ( ) func TestMarkdownToADF_Empty(t *testing.T) { + t.Parallel() result := MarkdownToADF("") testutil.Nil(t, result) } func TestMarkdownToADF_PlainText(t *testing.T) { + t.Parallel() result := MarkdownToADF("Hello world") testutil.NotNil(t, result) testutil.Equal(t, result.Type, "doc") diff --git a/tools/jtk/api/move_test.go b/tools/jtk/api/move_test.go index 2b772d9..54d5246 100644 --- a/tools/jtk/api/move_test.go +++ b/tools/jtk/api/move_test.go @@ -11,6 +11,7 @@ import ( ) func TestBuildMoveRequest(t *testing.T) { + t.Parallel() req := BuildMoveRequest([]string{"PROJ-1", "PROJ-2"}, "TARGET", "10001", true) testutil.True(t, req.SendBulkNotification) @@ -25,12 +26,14 @@ func TestBuildMoveRequest(t *testing.T) { } func TestBuildMoveRequest_NoNotify(t *testing.T) { + t.Parallel() req := BuildMoveRequest([]string{"PROJ-1"}, "TARGET", "10001", false) testutil.False(t, req.SendBulkNotification) } func TestGetMoveTaskStatus(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/bulk/queue/task-123") w.WriteHeader(http.StatusOK) @@ -67,6 +70,7 @@ func TestGetMoveTaskStatus(t *testing.T) { } func TestGetMoveTaskStatus_EmptyID(t *testing.T) { + t.Parallel() client, _ := New(ClientConfig{ URL: "http://unused", Email: "test@example.com", @@ -79,6 +83,7 @@ func TestGetMoveTaskStatus_EmptyID(t *testing.T) { } func TestGetMoveTaskStatus_WithFailures(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{ @@ -113,6 +118,7 @@ func TestGetMoveTaskStatus_WithFailures(t *testing.T) { } func TestMoveIssues(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) testutil.Equal(t, r.URL.Path, "/rest/api/3/bulk/issues/move") @@ -144,6 +150,7 @@ func TestMoveIssues(t *testing.T) { } func TestGetProjectIssueTypes(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/project/PROJ") w.WriteHeader(http.StatusOK) @@ -173,6 +180,7 @@ func TestGetProjectIssueTypes(t *testing.T) { } func TestGetProjectIssueTypes_EmptyProject(t *testing.T) { + t.Parallel() client, _ := New(ClientConfig{ URL: "http://unused", Email: "test@example.com", @@ -185,6 +193,7 @@ func TestGetProjectIssueTypes_EmptyProject(t *testing.T) { } func TestGetProjectStatuses(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/project/PROJ/statuses") w.WriteHeader(http.StatusOK) @@ -219,6 +228,7 @@ func TestGetProjectStatuses(t *testing.T) { } func TestGetProjectStatuses_EmptyProject(t *testing.T) { + t.Parallel() client, _ := New(ClientConfig{ URL: "http://unused", Email: "test@example.com", diff --git a/tools/jtk/api/projects_test.go b/tools/jtk/api/projects_test.go index eff34f3..f96440e 100644 --- a/tools/jtk/api/projects_test.go +++ b/tools/jtk/api/projects_test.go @@ -11,6 +11,7 @@ import ( ) func TestSearchProjects(t *testing.T) { + t.Parallel() tests := []struct { name string query string @@ -53,6 +54,7 @@ func TestSearchProjects(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/project/search") if tt.query != "" { diff --git a/tools/jtk/api/search.go b/tools/jtk/api/search.go index 9331ce8..83ce555 100644 --- a/tools/jtk/api/search.go +++ b/tools/jtk/api/search.go @@ -65,7 +65,7 @@ func (c *Client) Search(ctx context.Context, opts SearchOptions) (*SearchResult, urlStr := fmt.Sprintf("%s/search/jql", c.BaseURL) body, err := c.Post(ctx, urlStr, req) if err != nil { - return nil, err + return nil, fmt.Errorf("searching issues: %w", err) } var result SearchResult @@ -93,7 +93,7 @@ func (c *Client) SearchAll(ctx context.Context, jql string, maxResults int) ([]I MaxResults: pageSize, }) if err != nil { - return nil, err + return nil, fmt.Errorf("searching all issues (offset %d): %w", startAt, err) } allIssues = append(allIssues, result.Issues...) diff --git a/tools/jtk/api/sprints.go b/tools/jtk/api/sprints.go index a57547f..9c7ddca 100644 --- a/tools/jtk/api/sprints.go +++ b/tools/jtk/api/sprints.go @@ -80,7 +80,7 @@ func (c *Client) GetSprintIssues(ctx context.Context, sprintID int, startAt, max func (c *Client) GetCurrentSprint(ctx context.Context, boardID int) (*Sprint, error) { result, err := c.ListSprints(ctx, boardID, "active", 0, 1) if err != nil { - return nil, err + return nil, fmt.Errorf("getting current sprint for board %d: %w", boardID, err) } if len(result.Values) == 0 { diff --git a/tools/jtk/api/transitions_test.go b/tools/jtk/api/transitions_test.go index 153ad82..d389b81 100644 --- a/tools/jtk/api/transitions_test.go +++ b/tools/jtk/api/transitions_test.go @@ -12,6 +12,7 @@ import ( ) func TestFindTransitionByName(t *testing.T) { + t.Parallel() transitions := []Transition{ {ID: "11", Name: "To Do", To: Status{Name: "To Do"}}, {ID: "21", Name: "In Progress", To: Status{Name: "In Progress"}}, diff --git a/tools/jtk/api/types.go b/tools/jtk/api/types.go index 79b314a..a0f9088 100644 --- a/tools/jtk/api/types.go +++ b/tools/jtk/api/types.go @@ -2,6 +2,7 @@ package api //nolint:revive // package name is intentional import ( "encoding/json" + "fmt" "time" "github.com/open-cli-collective/atlassian-go/adf" @@ -55,13 +56,13 @@ func (f *IssueFields) UnmarshalJSON(data []byte) error { Alias: (*Alias)(f), } if err := json.Unmarshal(data, aux); err != nil { - return err + return fmt.Errorf("unmarshaling issue fields: %w", err) } // Then unmarshal into a map to capture all fields var raw map[string]json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { - return err + return fmt.Errorf("unmarshaling issue fields (raw): %w", err) } // Extract custom fields (those not in knownFieldKeys) diff --git a/tools/jtk/api/types_test.go b/tools/jtk/api/types_test.go index a477789..661e7eb 100644 --- a/tools/jtk/api/types_test.go +++ b/tools/jtk/api/types_test.go @@ -24,6 +24,7 @@ func jsonEq(t *testing.T, got, want string) { } func TestDescription_UnmarshalJSON(t *testing.T) { + t.Parallel() tests := []struct { name string input string diff --git a/tools/jtk/api/users.go b/tools/jtk/api/users.go index 134de08..c7bc11e 100644 --- a/tools/jtk/api/users.go +++ b/tools/jtk/api/users.go @@ -11,7 +11,7 @@ func (c *Client) GetCurrentUser(ctx context.Context) (*User, error) { urlStr := fmt.Sprintf("%s/myself", c.BaseURL) body, err := c.Get(ctx, urlStr) if err != nil { - return nil, err + return nil, fmt.Errorf("getting current user: %w", err) } var user User @@ -30,7 +30,7 @@ func (c *Client) GetUser(ctx context.Context, accountID string) (*User, error) { urlStr := buildURL(fmt.Sprintf("%s/user", c.BaseURL), params) body, err := c.Get(ctx, urlStr) if err != nil { - return nil, err + return nil, fmt.Errorf("getting user %s: %w", accountID, err) } var user User @@ -53,7 +53,7 @@ func (c *Client) SearchUsers(ctx context.Context, query string, maxResults int) urlStr := buildURL(fmt.Sprintf("%s/user/search", c.BaseURL), params) body, err := c.Get(ctx, urlStr) if err != nil { - return nil, err + return nil, fmt.Errorf("searching users: %w", err) } var users []User diff --git a/tools/jtk/api/users_test.go b/tools/jtk/api/users_test.go index 3e152e6..0fa3d5a 100644 --- a/tools/jtk/api/users_test.go +++ b/tools/jtk/api/users_test.go @@ -11,6 +11,7 @@ import ( ) func TestGetUser(t *testing.T) { + t.Parallel() tests := []struct { name string accountID string @@ -43,6 +44,7 @@ func TestGetUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/user") testutil.Equal(t, r.URL.Query().Get("accountId"), tt.accountID) @@ -72,6 +74,7 @@ func TestGetUser(t *testing.T) { } func TestGetCurrentUser(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/myself") user := User{ @@ -99,6 +102,7 @@ func TestGetCurrentUser(t *testing.T) { } func TestSearchUsers(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/user/search") testutil.Equal(t, r.URL.Query().Get("query"), "john") diff --git a/tools/jtk/api/wiki_test.go b/tools/jtk/api/wiki_test.go index 6bd48c0..76ef20b 100644 --- a/tools/jtk/api/wiki_test.go +++ b/tools/jtk/api/wiki_test.go @@ -7,6 +7,7 @@ import ( ) func TestIsWikiMarkup(t *testing.T) { + t.Parallel() tests := []struct { name string input string diff --git a/tools/jtk/internal/cmd/attachments/attachments_test.go b/tools/jtk/internal/cmd/attachments/attachments_test.go index c88d1e5..cdba02b 100644 --- a/tools/jtk/internal/cmd/attachments/attachments_test.go +++ b/tools/jtk/internal/cmd/attachments/attachments_test.go @@ -20,6 +20,7 @@ import ( // --- list tests --- func TestNewListCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newListCmd(opts) @@ -29,6 +30,7 @@ func TestNewListCmd(t *testing.T) { } func TestRunList_Table(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { response := struct { Fields struct { diff --git a/tools/jtk/internal/cmd/automation/create_test.go b/tools/jtk/internal/cmd/automation/create_test.go index 0014c0f..c2a48cc 100644 --- a/tools/jtk/internal/cmd/automation/create_test.go +++ b/tools/jtk/internal/cmd/automation/create_test.go @@ -17,7 +17,9 @@ import ( ) func TestRunCreate(t *testing.T) { + t.Parallel() t.Run("strips server-assigned fields", func(t *testing.T) { + t.Parallel() var receivedBody map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/tools/jtk/internal/cmd/automation/enable_test.go b/tools/jtk/internal/cmd/automation/enable_test.go index 6123455..7b24725 100644 --- a/tools/jtk/internal/cmd/automation/enable_test.go +++ b/tools/jtk/internal/cmd/automation/enable_test.go @@ -29,6 +29,7 @@ func newAutomationTestServer(t *testing.T, rule api.AutomationRule) *httptest.Se } func TestRunSetState_AlreadyEnabled(t *testing.T) { + t.Parallel() rule := api.AutomationRule{ ID: json.Number("42"), Name: "Test Rule", @@ -59,6 +60,7 @@ func TestRunSetState_AlreadyEnabled(t *testing.T) { } func TestRunSetState_AlreadyDisabled(t *testing.T) { + t.Parallel() rule := api.AutomationRule{ ID: json.Number("42"), Name: "Test Rule", @@ -89,6 +91,7 @@ func TestRunSetState_AlreadyDisabled(t *testing.T) { } func TestRunSetState_EnableDisabledRule(t *testing.T) { + t.Parallel() requestCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/_edge/tenant_info" { diff --git a/tools/jtk/internal/cmd/automation/get_test.go b/tools/jtk/internal/cmd/automation/get_test.go index 08992e7..daf98bc 100644 --- a/tools/jtk/internal/cmd/automation/get_test.go +++ b/tools/jtk/internal/cmd/automation/get_test.go @@ -9,6 +9,7 @@ import ( ) func TestSummarizeComponents(t *testing.T) { + t.Parallel() tests := []struct { name string components []api.RuleComponent @@ -57,6 +58,7 @@ func TestSummarizeComponents(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() got := summarizeComponents(tt.components) testutil.Equal(t, got, tt.want) }) diff --git a/tools/jtk/internal/cmd/automation/update_test.go b/tools/jtk/internal/cmd/automation/update_test.go index 18b04d5..50c7349 100644 --- a/tools/jtk/internal/cmd/automation/update_test.go +++ b/tools/jtk/internal/cmd/automation/update_test.go @@ -13,7 +13,9 @@ import ( ) func TestRunUpdate(t *testing.T) { + t.Parallel() t.Run("invalid JSON file", func(t *testing.T) { + t.Parallel() dir := t.TempDir() filePath := filepath.Join(dir, "bad.json") err := os.WriteFile(filePath, []byte(`not valid json`), 0600) @@ -32,6 +34,7 @@ func TestRunUpdate(t *testing.T) { }) t.Run("file not found", func(t *testing.T) { + t.Parallel() var stdout, stderr bytes.Buffer opts := &root.Options{ Output: "table", diff --git a/tools/jtk/internal/cmd/boards/boards_test.go b/tools/jtk/internal/cmd/boards/boards_test.go index 9d36ad4..89ca21d 100644 --- a/tools/jtk/internal/cmd/boards/boards_test.go +++ b/tools/jtk/internal/cmd/boards/boards_test.go @@ -15,6 +15,7 @@ import ( ) func TestNewListCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newListCmd(opts) @@ -31,6 +32,7 @@ func TestNewListCmd(t *testing.T) { } func TestRunList_Table(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.BoardsResponse{ Values: []api.Board{ @@ -79,6 +81,7 @@ func TestRunList_Table(t *testing.T) { } func TestRunList_JSON(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.BoardsResponse{ Values: []api.Board{ @@ -113,6 +116,7 @@ func TestRunList_JSON(t *testing.T) { } func TestRunList_Empty(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.BoardsResponse{ Values: []api.Board{}, @@ -137,6 +141,7 @@ func TestRunList_Empty(t *testing.T) { } func TestRunGet_Table(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.Board{ ID: 42, @@ -167,6 +172,7 @@ func TestRunGet_Table(t *testing.T) { } func TestRunGet_JSON(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.Board{ ID: 42, @@ -195,6 +201,7 @@ func TestRunGet_JSON(t *testing.T) { } func TestRunGet_InvalidID(t *testing.T) { + t.Parallel() rootCmd, opts := root.NewCmd() Register(rootCmd, opts) diff --git a/tools/jtk/internal/cmd/comments/comments_test.go b/tools/jtk/internal/cmd/comments/comments_test.go index 2769ace..445b06c 100644 --- a/tools/jtk/internal/cmd/comments/comments_test.go +++ b/tools/jtk/internal/cmd/comments/comments_test.go @@ -16,6 +16,7 @@ import ( ) func TestNewListCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newListCmd(opts) @@ -46,6 +47,7 @@ func newTestCommentsServer(_ *testing.T, comments []api.Comment) *httptest.Serve } func TestRunList_TruncatesCommentBody(t *testing.T) { + t.Parallel() longText := strings.Repeat("B", 200) comments := []api.Comment{ { @@ -95,6 +97,7 @@ func TestRunList_TruncatesCommentBody(t *testing.T) { } func TestRunList_FullCommentBody(t *testing.T) { + t.Parallel() longText := strings.Repeat("B", 200) comments := []api.Comment{ { @@ -147,6 +150,7 @@ func TestRunList_FullCommentBody(t *testing.T) { } func TestRunList_ShortCommentNotTruncated(t *testing.T) { + t.Parallel() comments := []api.Comment{ { ID: "1", @@ -194,6 +198,7 @@ func TestRunList_ShortCommentNotTruncated(t *testing.T) { } func TestRunList_NoComments(t *testing.T) { + t.Parallel() server := newTestCommentsServer(t, []api.Comment{}) defer server.Close() @@ -220,6 +225,7 @@ func TestRunList_NoComments(t *testing.T) { } func TestRunList_MultipleCommentsFullMode(t *testing.T) { + t.Parallel() comments := []api.Comment{ { ID: "1", diff --git a/tools/jtk/internal/cmd/completion/completion_test.go b/tools/jtk/internal/cmd/completion/completion_test.go index 89e0017..9c3333f 100644 --- a/tools/jtk/internal/cmd/completion/completion_test.go +++ b/tools/jtk/internal/cmd/completion/completion_test.go @@ -37,6 +37,7 @@ func captureStdout(t *testing.T, fn func()) string { } func TestNewCompletionCmd(t *testing.T) { + t.Parallel() rootCmd := newTestRootCmd() cmd, _, err := rootCmd.Find([]string{"completion"}) diff --git a/tools/jtk/internal/cmd/fields/fields_test.go b/tools/jtk/internal/cmd/fields/fields_test.go index 5b968ff..be4b04b 100644 --- a/tools/jtk/internal/cmd/fields/fields_test.go +++ b/tools/jtk/internal/cmd/fields/fields_test.go @@ -15,6 +15,7 @@ import ( ) func TestRegister(t *testing.T) { + t.Parallel() rootCmd, opts := root.NewCmd() Register(rootCmd, opts) @@ -25,6 +26,7 @@ func TestRegister(t *testing.T) { } func TestNewListCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newListCmd(opts) @@ -37,6 +39,7 @@ func TestNewListCmd(t *testing.T) { } func TestRunList_Table(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode([]api.Field{ {ID: "summary", Name: "Summary", Schema: api.FieldSchema{Type: "string"}}, @@ -60,6 +63,7 @@ func TestRunList_Table(t *testing.T) { } func TestRunList_JSON(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode([]api.Field{ {ID: "customfield_10100", Name: "Environment", Custom: true}, @@ -81,6 +85,7 @@ func TestRunList_JSON(t *testing.T) { } func TestRunList_Empty(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode([]api.Field{}) })) @@ -99,6 +104,7 @@ func TestRunList_Empty(t *testing.T) { } func TestNewCreateCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newCreateCmd(opts) @@ -115,6 +121,7 @@ func TestNewCreateCmd(t *testing.T) { } func TestRunCreate(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) w.WriteHeader(http.StatusCreated) @@ -140,6 +147,7 @@ func TestRunCreate(t *testing.T) { } func TestRunCreate_JSON(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(api.Field{ @@ -163,6 +171,7 @@ func TestRunCreate_JSON(t *testing.T) { } func TestNewDeleteCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newDeleteCmd(opts) @@ -174,6 +183,7 @@ func TestNewDeleteCmd(t *testing.T) { } func TestRunDelete_Force(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) testutil.Contains(t, r.URL.Path, "/trash") @@ -194,6 +204,7 @@ func TestRunDelete_Force(t *testing.T) { } func TestRunDelete_NoForce_Declined(t *testing.T) { + t.Parallel() client, err := api.New(api.ClientConfig{URL: "https://test.atlassian.net", Email: "test@test.com", APIToken: "token"}) testutil.RequireNoError(t, err) @@ -212,6 +223,7 @@ func TestRunDelete_NoForce_Declined(t *testing.T) { } func TestRunDelete_NoForce_Accepted(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) w.WriteHeader(http.StatusOK) @@ -236,6 +248,7 @@ func TestRunDelete_NoForce_Accepted(t *testing.T) { } func TestRunRestore(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) testutil.Contains(t, r.URL.Path, "/restore") @@ -258,6 +271,7 @@ func TestRunRestore(t *testing.T) { // --- Contexts tests --- func TestNewContextsCmd(t *testing.T) { + t.Parallel() rootCmd, opts := root.NewCmd() Register(rootCmd, opts) @@ -268,6 +282,7 @@ func TestNewContextsCmd(t *testing.T) { } func TestRunContextsList_Table(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.FieldContextsResponse{ Values: []api.FieldContext{ @@ -292,6 +307,7 @@ func TestRunContextsList_Table(t *testing.T) { } func TestRunContextsList_Empty(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.FieldContextsResponse{Values: []api.FieldContext{}}) })) @@ -310,6 +326,7 @@ func TestRunContextsList_Empty(t *testing.T) { } func TestRunContextsCreate(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPost) w.WriteHeader(http.StatusCreated) @@ -334,6 +351,7 @@ func TestRunContextsCreate(t *testing.T) { } func TestRunContextsDelete_Force(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodDelete) w.WriteHeader(http.StatusNoContent) @@ -353,6 +371,7 @@ func TestRunContextsDelete_Force(t *testing.T) { } func TestRunContextsDelete_NoForce_Declined(t *testing.T) { + t.Parallel() client, err := api.New(api.ClientConfig{URL: "https://test.atlassian.net", Email: "test@test.com", APIToken: "token"}) testutil.RequireNoError(t, err) @@ -373,6 +392,7 @@ func TestRunContextsDelete_NoForce_Declined(t *testing.T) { // --- Options tests --- func TestNewOptionsCmd(t *testing.T) { + t.Parallel() rootCmd, opts := root.NewCmd() Register(rootCmd, opts) @@ -383,6 +403,7 @@ func TestNewOptionsCmd(t *testing.T) { } func TestResolveContextID_Explicit(t *testing.T) { + t.Parallel() // When context flag is provided, it should be used directly id, err := resolveContextID(context.Background(), nil, "customfield_10100", "10001") testutil.RequireNoError(t, err) @@ -390,6 +411,7 @@ func TestResolveContextID_Explicit(t *testing.T) { } func TestResolveContextID_AutoDetect(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.FieldContextsResponse{ Values: []api.FieldContext{ @@ -408,6 +430,7 @@ func TestResolveContextID_AutoDetect(t *testing.T) { } func TestRunOptionsList_Table(t *testing.T) { + t.Parallel() callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { callCount++ @@ -442,6 +465,7 @@ func TestRunOptionsList_Table(t *testing.T) { } func TestRunOptionsList_Empty(t *testing.T) { + t.Parallel() callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { callCount++ @@ -468,6 +492,7 @@ func TestRunOptionsList_Empty(t *testing.T) { } func TestRunOptionsAdd(t *testing.T) { + t.Parallel() callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ @@ -500,6 +525,7 @@ func TestRunOptionsAdd(t *testing.T) { } func TestRunOptionsUpdate(t *testing.T) { + t.Parallel() callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ @@ -531,6 +557,7 @@ func TestRunOptionsUpdate(t *testing.T) { } func TestRunOptionsDelete_Force(t *testing.T) { + t.Parallel() callCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ @@ -558,6 +585,7 @@ func TestRunOptionsDelete_Force(t *testing.T) { } func TestRunOptionsDelete_NoForce_Declined(t *testing.T) { + t.Parallel() client, err := api.New(api.ClientConfig{URL: "https://test.atlassian.net", Email: "test@test.com", APIToken: "token"}) testutil.RequireNoError(t, err) diff --git a/tools/jtk/internal/cmd/issues/assignee_test.go b/tools/jtk/internal/cmd/issues/assignee_test.go index d113337..4bb1b44 100644 --- a/tools/jtk/internal/cmd/issues/assignee_test.go +++ b/tools/jtk/internal/cmd/issues/assignee_test.go @@ -13,6 +13,7 @@ import ( ) func TestResolveAssignee_RawAccountID(t *testing.T) { + t.Parallel() client, err := api.New(api.ClientConfig{ URL: "http://unused", Email: "test@example.com", @@ -26,6 +27,7 @@ func TestResolveAssignee_RawAccountID(t *testing.T) { } func TestResolveAssignee_Me(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/rest/api/3/myself" { _ = json.NewEncoder(w).Encode(api.User{ @@ -51,6 +53,7 @@ func TestResolveAssignee_Me(t *testing.T) { } func TestResolveAssignee_MeCaseInsensitive(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/rest/api/3/myself" { _ = json.NewEncoder(w).Encode(api.User{ @@ -76,6 +79,7 @@ func TestResolveAssignee_MeCaseInsensitive(t *testing.T) { } func TestResolveAssignee_Email(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/rest/api/3/user/search" { _ = json.NewEncoder(w).Encode([]api.User{ @@ -100,6 +104,7 @@ func TestResolveAssignee_Email(t *testing.T) { } func TestResolveAssignee_EmailNotFound(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/rest/api/3/user/search" { _ = json.NewEncoder(w).Encode([]api.User{}) diff --git a/tools/jtk/internal/cmd/issues/create_test.go b/tools/jtk/internal/cmd/issues/create_test.go index dfda4c3..ad0949d 100644 --- a/tools/jtk/internal/cmd/issues/create_test.go +++ b/tools/jtk/internal/cmd/issues/create_test.go @@ -16,6 +16,7 @@ import ( ) func TestRunCreate_RequestBodyNoDoubleQuoting(t *testing.T) { + t.Parallel() var capturedBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/tools/jtk/internal/cmd/issues/get_test.go b/tools/jtk/internal/cmd/issues/get_test.go index 48820f4..c639205 100644 --- a/tools/jtk/internal/cmd/issues/get_test.go +++ b/tools/jtk/internal/cmd/issues/get_test.go @@ -16,6 +16,7 @@ import ( ) func TestNewGetCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newGetCmd(opts) @@ -36,6 +37,7 @@ func newTestIssueServer(_ *testing.T, issue api.Issue) *httptest.Server { } func TestRunGet_TruncatesDescription(t *testing.T) { + t.Parallel() longText := strings.Repeat("A", 300) issue := api.Issue{ Key: "TEST-1", @@ -75,6 +77,7 @@ func TestRunGet_TruncatesDescription(t *testing.T) { } func TestRunGet_FullDescription(t *testing.T) { + t.Parallel() longText := strings.Repeat("A", 300) issue := api.Issue{ Key: "TEST-1", @@ -113,6 +116,7 @@ func TestRunGet_FullDescription(t *testing.T) { } func TestRunGet_ShortDescriptionNotTruncated(t *testing.T) { + t.Parallel() issue := api.Issue{ Key: "TEST-1", Fields: api.IssueFields{ @@ -150,6 +154,7 @@ func TestRunGet_ShortDescriptionNotTruncated(t *testing.T) { } func TestRunGet_JSONOutputIgnoresFullFlag(t *testing.T) { + t.Parallel() issue := api.Issue{ Key: "TEST-1", Fields: api.IssueFields{ diff --git a/tools/jtk/internal/cmd/issues/types_test.go b/tools/jtk/internal/cmd/issues/types_test.go index 2116d49..de56370 100644 --- a/tools/jtk/internal/cmd/issues/types_test.go +++ b/tools/jtk/internal/cmd/issues/types_test.go @@ -16,6 +16,7 @@ import ( ) func TestNewTypesCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newTypesCmd(opts) @@ -29,6 +30,7 @@ func TestNewTypesCmd(t *testing.T) { } func TestRunTypes_Success(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.URL.Path, "/rest/api/3/project/TEST") @@ -73,6 +75,7 @@ func TestRunTypes_Success(t *testing.T) { } func TestRunTypes_ProjectNotFound(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"errorMessages":["No project could be found with key 'INVALID'."]}`)) @@ -99,6 +102,7 @@ func TestRunTypes_ProjectNotFound(t *testing.T) { } func TestRunTypes_EmptyIssueTypes(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { response := api.ProjectDetail{ ID: json.Number("10000"), @@ -132,6 +136,7 @@ func TestRunTypes_EmptyIssueTypes(t *testing.T) { } func TestRunTypes_JSONOutput(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { response := api.ProjectDetail{ ID: json.Number("10000"), @@ -178,6 +183,7 @@ func TestRunTypes_JSONOutput(t *testing.T) { } func TestRunTypes_DescriptionTruncation(t *testing.T) { + t.Parallel() longDesc := strings.Repeat("A", 100) // 100 character description server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { diff --git a/tools/jtk/internal/cmd/issues/update_test.go b/tools/jtk/internal/cmd/issues/update_test.go index 6dfe2ee..ec350ca 100644 --- a/tools/jtk/internal/cmd/issues/update_test.go +++ b/tools/jtk/internal/cmd/issues/update_test.go @@ -16,6 +16,7 @@ import ( ) func TestRunUpdate_RequestBodyNoDoubleQuoting(t *testing.T) { + t.Parallel() var capturedBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/tools/jtk/internal/cmd/me/me_test.go b/tools/jtk/internal/cmd/me/me_test.go index ea58d5f..854459e 100644 --- a/tools/jtk/internal/cmd/me/me_test.go +++ b/tools/jtk/internal/cmd/me/me_test.go @@ -15,6 +15,7 @@ import ( ) func TestNewMeCmd(t *testing.T) { + t.Parallel() rootCmd, opts := root.NewCmd() Register(rootCmd, opts) @@ -34,6 +35,7 @@ func newTestUserServer(_ *testing.T, statusCode int, user *api.User) *httptest.S } func TestRun_Table(t *testing.T) { + t.Parallel() user := &api.User{ AccountID: "abc123", DisplayName: "John Doe", @@ -62,6 +64,7 @@ func TestRun_Table(t *testing.T) { } func TestRun_JSON(t *testing.T) { + t.Parallel() user := &api.User{ AccountID: "abc123", DisplayName: "John Doe", @@ -90,6 +93,7 @@ func TestRun_JSON(t *testing.T) { } func TestRun_WithEmail(t *testing.T) { + t.Parallel() user := &api.User{ AccountID: "abc123", DisplayName: "John Doe", @@ -114,6 +118,7 @@ func TestRun_WithEmail(t *testing.T) { } func TestRun_WithoutEmail(t *testing.T) { + t.Parallel() user := &api.User{ AccountID: "abc123", DisplayName: "John Doe", @@ -138,6 +143,7 @@ func TestRun_WithoutEmail(t *testing.T) { } func TestRun_Plain(t *testing.T) { + t.Parallel() user := &api.User{ AccountID: "abc123", DisplayName: "John Doe", @@ -163,6 +169,7 @@ func TestRun_Plain(t *testing.T) { } func TestRun_AuthFailure(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"message":"Unauthorized"}`)) diff --git a/tools/jtk/internal/cmd/projects/projects_test.go b/tools/jtk/internal/cmd/projects/projects_test.go index b8f77bc..b782bb2 100644 --- a/tools/jtk/internal/cmd/projects/projects_test.go +++ b/tools/jtk/internal/cmd/projects/projects_test.go @@ -15,6 +15,7 @@ import ( ) func TestRegister(t *testing.T) { + t.Parallel() rootCmd, opts := root.NewCmd() Register(rootCmd, opts) @@ -25,6 +26,7 @@ func TestRegister(t *testing.T) { } func TestNewListCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newListCmd(opts) @@ -41,6 +43,7 @@ func TestNewListCmd(t *testing.T) { } func TestRunList_Table(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.ProjectSearchResponse{ Values: []api.ProjectDetail{ @@ -66,6 +69,7 @@ func TestRunList_Table(t *testing.T) { } func TestRunList_JSON(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.ProjectSearchResponse{ Values: []api.ProjectDetail{ @@ -91,6 +95,7 @@ func TestRunList_JSON(t *testing.T) { } func TestRunList_Empty(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.ProjectSearchResponse{Values: []api.ProjectDetail{}, Total: 0, IsLast: true}) })) @@ -109,6 +114,7 @@ func TestRunList_Empty(t *testing.T) { } func TestNewGetCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newGetCmd(opts) @@ -116,6 +122,7 @@ func TestNewGetCmd(t *testing.T) { } func TestRunGet_Table(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.ProjectDetail{ ID: json.Number("10001"), @@ -141,6 +148,7 @@ func TestRunGet_Table(t *testing.T) { } func TestNewCreateCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newCreateCmd(opts) @@ -157,6 +165,7 @@ func TestNewCreateCmd(t *testing.T) { } func TestRunCreate(t *testing.T) { + t.Parallel() // Jira's create endpoint returns an empty name, so the success message // should use the input name, not the response name. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -183,6 +192,7 @@ func TestRunCreate(t *testing.T) { } func TestNewDeleteCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newDeleteCmd(opts) @@ -212,6 +222,7 @@ func TestRunDelete_Force(t *testing.T) { } func TestRunDelete_NoForce_Declined(t *testing.T) { + t.Parallel() client, err := api.New(api.ClientConfig{URL: "https://test.atlassian.net", Email: "test@test.com", APIToken: "token"}) testutil.RequireNoError(t, err) @@ -230,6 +241,7 @@ func TestRunDelete_NoForce_Declined(t *testing.T) { } func TestRunDelete_NoForce_Accepted(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodDelete) w.WriteHeader(http.StatusNoContent) @@ -254,6 +266,7 @@ func TestRunDelete_NoForce_Accepted(t *testing.T) { } func TestRunUpdate(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testutil.Equal(t, r.Method, http.MethodPut) _ = json.NewEncoder(w).Encode(api.ProjectDetail{ @@ -277,6 +290,7 @@ func TestRunUpdate(t *testing.T) { } func TestRunRestore(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(api.ProjectDetail{ ID: json.Number("10001"), @@ -299,6 +313,7 @@ func TestRunRestore(t *testing.T) { } func TestRunTypes(t *testing.T) { + t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode([]api.ProjectType{ {Key: "software", FormattedKey: "Software"}, diff --git a/tools/jtk/internal/cmd/root/root_test.go b/tools/jtk/internal/cmd/root/root_test.go index e251096..e31e4e5 100644 --- a/tools/jtk/internal/cmd/root/root_test.go +++ b/tools/jtk/internal/cmd/root/root_test.go @@ -11,6 +11,7 @@ import ( ) func TestNewCmd(t *testing.T) { + t.Parallel() cmd, opts := NewCmd() testutil.Equal(t, cmd.Use, "jtk") @@ -30,6 +31,7 @@ func TestNewCmd(t *testing.T) { } func TestNewCmd_Flags(t *testing.T) { + t.Parallel() cmd, _ := NewCmd() tests := []struct { @@ -43,6 +45,7 @@ func TestNewCmd_Flags(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() f := cmd.PersistentFlags().Lookup(tt.flag) testutil.NotNil(t, f) }) @@ -50,6 +53,7 @@ func TestNewCmd_Flags(t *testing.T) { } func TestNewCmd_FlagDefaults(t *testing.T) { + t.Parallel() cmd, _ := NewCmd() outputFlag := cmd.PersistentFlags().Lookup("output") @@ -63,6 +67,7 @@ func TestNewCmd_FlagDefaults(t *testing.T) { } func TestOptions_View(t *testing.T) { + t.Parallel() var stdout, stderr bytes.Buffer opts := &Options{ Output: "json", diff --git a/tools/jtk/internal/cmd/sprints/sprints_test.go b/tools/jtk/internal/cmd/sprints/sprints_test.go index 4903fe6..78282ce 100644 --- a/tools/jtk/internal/cmd/sprints/sprints_test.go +++ b/tools/jtk/internal/cmd/sprints/sprints_test.go @@ -19,6 +19,7 @@ import ( // --- list subcommand --- func TestNewListCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newListCmd(opts) @@ -52,6 +53,7 @@ func newTestSprintsServer(_ *testing.T, sprints []api.Sprint) *httptest.Server { } func TestRunList_Table(t *testing.T) { + t.Parallel() start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2025, 1, 14, 0, 0, 0, 0, time.UTC) sprints := []api.Sprint{ diff --git a/tools/jtk/internal/cmd/transitions/transitions_test.go b/tools/jtk/internal/cmd/transitions/transitions_test.go index 404ee0a..7c6d743 100644 --- a/tools/jtk/internal/cmd/transitions/transitions_test.go +++ b/tools/jtk/internal/cmd/transitions/transitions_test.go @@ -9,6 +9,7 @@ import ( ) func TestFormatFieldValue(t *testing.T) { + t.Parallel() tests := []struct { name string field *api.Field diff --git a/tools/jtk/internal/cmd/users/users_test.go b/tools/jtk/internal/cmd/users/users_test.go index 793f2e7..54e82af 100644 --- a/tools/jtk/internal/cmd/users/users_test.go +++ b/tools/jtk/internal/cmd/users/users_test.go @@ -15,6 +15,7 @@ import ( ) func TestNewSearchCmd(t *testing.T) { + t.Parallel() opts := &root.Options{} cmd := newSearchCmd(opts) @@ -34,6 +35,7 @@ func newTestUsersServer(_ *testing.T, users []api.User) *httptest.Server { } func TestRunSearch_Table(t *testing.T) { + t.Parallel() users := []api.User{ {AccountID: "abc123", DisplayName: "John Doe", EmailAddress: "john@example.com", Active: true}, {AccountID: "def456", DisplayName: "Jane Smith", EmailAddress: "jane@example.com", Active: false}, @@ -61,6 +63,7 @@ func TestRunSearch_Table(t *testing.T) { } func TestRunSearch_JSON(t *testing.T) { + t.Parallel() users := []api.User{ {AccountID: "abc123", DisplayName: "John Doe", EmailAddress: "john@example.com", Active: true}, } @@ -85,6 +88,7 @@ func TestRunSearch_JSON(t *testing.T) { } func TestRunSearch_Empty(t *testing.T) { + t.Parallel() server := newTestUsersServer(t, []api.User{}) defer server.Close() @@ -102,6 +106,7 @@ func TestRunSearch_Empty(t *testing.T) { } func TestRunSearch_ActiveUser(t *testing.T) { + t.Parallel() users := []api.User{ {AccountID: "abc123", DisplayName: "John Doe", Active: true}, } @@ -123,6 +128,7 @@ func TestRunSearch_ActiveUser(t *testing.T) { } func TestRunSearch_InactiveUser(t *testing.T) { + t.Parallel() users := []api.User{ {AccountID: "abc123", DisplayName: "John Doe", Active: false}, } From 9fb38bee8472293978224f0b86bda42f326d40ea Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 19 Feb 2026 07:29:42 -0500 Subject: [PATCH 13/14] refactor: align remaining code with STANDARDS.md - Replace interface{} with any across shared packages (client, view, testutil, adf) - Add ctx.Err() checks to pagination loops in SearchAll and ListAutomationRules for cancellation responsiveness - Add t.Parallel() to test functions without shared mutable state - Fix race condition in automation_test.go pagination counter using sync/atomic - Add package doc comment to jtk api package - Wrap bare error return in APIError.UnmarshalJSON --- shared/adf/convert.go | 16 ++++----- shared/adf/types.go | 14 ++++---- shared/client/client.go | 6 ++-- shared/errors/errors.go | 2 +- shared/testutil/assert.go | 20 +++++------ shared/view/view.go | 16 ++++----- shared/view/view_test.go | 36 +++++++++++++++++++ tools/jtk/api/automation.go | 4 +++ tools/jtk/api/automation_test.go | 32 +++++++++++++---- tools/jtk/api/client.go | 1 + tools/jtk/api/search.go | 4 +++ tools/jtk/api/search_test.go | 25 +++++++++++++ .../internal/cmd/configcmd/configcmd_test.go | 2 ++ .../jtk/internal/cmd/initcmd/initcmd_test.go | 1 + tools/jtk/internal/config/config_test.go | 3 ++ 15 files changed, 139 insertions(+), 43 deletions(-) create mode 100644 tools/jtk/api/search_test.go diff --git a/shared/adf/convert.go b/shared/adf/convert.go index 83bad11..9f49e8c 100644 --- a/shared/adf/convert.go +++ b/shared/adf/convert.go @@ -143,17 +143,17 @@ func (c *converter) convertParagraph(n *ast.Paragraph) *Node { func (c *converter) convertHeading(n *ast.Heading) *Node { return &Node{ Type: "heading", - Attrs: map[string]interface{}{"level": n.Level}, + Attrs: map[string]any{"level": n.Level}, Content: c.convertInlineChildren(n), } } func (c *converter) convertList(n *ast.List) *Node { listType := "bulletList" - var attrs map[string]interface{} + var attrs map[string]any if n.IsOrdered() { listType = "orderedList" - attrs = map[string]interface{}{"order": n.Start} + attrs = map[string]any{"order": n.Start} } return &Node{ @@ -224,7 +224,7 @@ func (c *converter) convertFencedCodeBlock(n *ast.FencedCodeBlock) *Node { } if lang := string(n.Language(c.source)); lang != "" { - node.Attrs = map[string]interface{}{"language": lang} + node.Attrs = map[string]any{"language": lang} } return node @@ -271,7 +271,7 @@ func (c *converter) convertTable(n *extast.Table) *Node { return &Node{ Type: "table", - Attrs: map[string]interface{}{"layout": "default"}, + Attrs: map[string]any{"layout": "default"}, Content: rows, } } @@ -318,7 +318,7 @@ func (c *converter) convertTableCell(n *extast.TableCell, isHeader bool) *Node { return &Node{ Type: cellType, - Attrs: map[string]interface{}{"colspan": 1, "rowspan": 1}, + Attrs: map[string]any{"colspan": 1, "rowspan": 1}, Content: []*Node{para}, } } @@ -397,7 +397,7 @@ func (c *converter) convertInlineNode(n ast.Node, marks []*Mark) []*Node { case *ast.Link: linkMark := &Mark{ Type: "link", - Attrs: map[string]interface{}{"href": string(node.Destination)}, + Attrs: map[string]any{"href": string(node.Destination)}, } newMarks := append(copyMarks(marks), linkMark) var nodes []*Node @@ -410,7 +410,7 @@ func (c *converter) convertInlineNode(n ast.Node, marks []*Mark) []*Node { url := string(node.URL(c.source)) linkMark := &Mark{ Type: "link", - Attrs: map[string]interface{}{"href": url}, + Attrs: map[string]any{"href": url}, } newMarks := append(copyMarks(marks), linkMark) return []*Node{{Type: "text", Text: url, Marks: newMarks}} diff --git a/shared/adf/types.go b/shared/adf/types.go index d88d26d..127f27b 100644 --- a/shared/adf/types.go +++ b/shared/adf/types.go @@ -11,17 +11,17 @@ type Document struct { // Node represents a node in an ADF document. type Node struct { - Type string `json:"type"` - Attrs map[string]interface{} `json:"attrs,omitempty"` - Content []*Node `json:"content,omitempty"` - Text string `json:"text,omitempty"` - Marks []*Mark `json:"marks,omitempty"` + Type string `json:"type"` + Attrs map[string]any `json:"attrs,omitempty"` + Content []*Node `json:"content,omitempty"` + Text string `json:"text,omitempty"` + Marks []*Mark `json:"marks,omitempty"` } // Mark represents text formatting (bold, italic, link, etc.) in ADF. type Mark struct { - Type string `json:"type"` - Attrs map[string]interface{} `json:"attrs,omitempty"` + Type string `json:"type"` + Attrs map[string]any `json:"attrs,omitempty"` } // ToPlainText extracts plain text from a Document. diff --git a/shared/client/client.go b/shared/client/client.go index 34582a8..dd445d1 100644 --- a/shared/client/client.go +++ b/shared/client/client.go @@ -69,7 +69,7 @@ func New(baseURL, email, apiToken string, opts *Options) *Client { // or an absolute URL (e.g., "https://example.com/api/resource"). // If body is not nil, it will be JSON-encoded. // Returns the response body or an error (which may be an *errors.APIError). -func (c *Client) Do(ctx context.Context, method, path string, body interface{}) ([]byte, error) { +func (c *Client) Do(ctx context.Context, method, path string, body any) ([]byte, error) { var url string // Check if path is an absolute URL @@ -135,12 +135,12 @@ func (c *Client) Get(ctx context.Context, path string) ([]byte, error) { } // Post performs a POST request with a JSON body. -func (c *Client) Post(ctx context.Context, path string, body interface{}) ([]byte, error) { +func (c *Client) Post(ctx context.Context, path string, body any) ([]byte, error) { return c.Do(ctx, http.MethodPost, path, body) } // Put performs a PUT request with a JSON body. -func (c *Client) Put(ctx context.Context, path string, body interface{}) ([]byte, error) { +func (c *Client) Put(ctx context.Context, path string, body any) ([]byte, error) { return c.Do(ctx, http.MethodPut, path, body) } diff --git a/shared/errors/errors.go b/shared/errors/errors.go index 2ad1436..deb1c33 100644 --- a/shared/errors/errors.go +++ b/shared/errors/errors.go @@ -43,7 +43,7 @@ func (e *APIError) UnmarshalJSON(data []byte) error { } if err := json.Unmarshal(data, aux); err != nil { - return err + return fmt.Errorf("unmarshaling API error: %w", err) } // Handle "errors" field which can be object or array diff --git a/shared/testutil/assert.go b/shared/testutil/assert.go index b14c6e0..78510f0 100644 --- a/shared/testutil/assert.go +++ b/shared/testutil/assert.go @@ -13,7 +13,7 @@ import ( ) // Equal checks that got and want are deeply equal. -func Equal(t *testing.T, got, want interface{}) { +func Equal(t *testing.T, got, want any) { t.Helper() if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) @@ -21,7 +21,7 @@ func Equal(t *testing.T, got, want interface{}) { } // RequireEqual checks that got and want are deeply equal, stopping the test on failure. -func RequireEqual(t *testing.T, got, want interface{}) { +func RequireEqual(t *testing.T, got, want any) { t.Helper() if !reflect.DeepEqual(got, want) { t.Fatalf("got %v, want %v", got, want) @@ -69,7 +69,7 @@ func Contains(t *testing.T, s, substr string) { } // True checks that the condition is true. -func True(t *testing.T, condition bool, msgAndArgs ...interface{}) { +func True(t *testing.T, condition bool, msgAndArgs ...any) { t.Helper() if !condition { if len(msgAndArgs) > 0 { @@ -81,7 +81,7 @@ func True(t *testing.T, condition bool, msgAndArgs ...interface{}) { } // False checks that the condition is false. -func False(t *testing.T, condition bool, msgAndArgs ...interface{}) { +func False(t *testing.T, condition bool, msgAndArgs ...any) { t.Helper() if condition { if len(msgAndArgs) > 0 { @@ -93,7 +93,7 @@ func False(t *testing.T, condition bool, msgAndArgs ...interface{}) { } // Nil checks that v is nil. -func Nil(t *testing.T, v interface{}) { +func Nil(t *testing.T, v any) { t.Helper() if v == nil { return @@ -110,7 +110,7 @@ func Nil(t *testing.T, v interface{}) { } // NotNil checks that v is not nil. -func NotNil(t *testing.T, v interface{}) { +func NotNil(t *testing.T, v any) { t.Helper() if v == nil { t.Errorf("expected not nil, got nil") @@ -128,7 +128,7 @@ func NotNil(t *testing.T, v interface{}) { // Len checks that the length of v equals expected. // v must be a string, slice, array, map, or channel. -func Len(t *testing.T, v interface{}, expected int) { +func Len(t *testing.T, v any, expected int) { t.Helper() rv := reflect.ValueOf(v) switch rv.Kind() { //nolint:exhaustive // covered by default case @@ -142,7 +142,7 @@ func Len(t *testing.T, v interface{}, expected int) { } // Empty checks that v has length 0. -func Empty(t *testing.T, v interface{}) { +func Empty(t *testing.T, v any) { t.Helper() rv := reflect.ValueOf(v) switch rv.Kind() { //nolint:exhaustive // covered by default case @@ -156,7 +156,7 @@ func Empty(t *testing.T, v interface{}) { } // NotEmpty checks that v has length > 0. -func NotEmpty(t *testing.T, v interface{}) { +func NotEmpty(t *testing.T, v any) { t.Helper() rv := reflect.ValueOf(v) switch rv.Kind() { //nolint:exhaustive // covered by default case @@ -210,7 +210,7 @@ func HasSuffix(t *testing.T, s, suffix string) { } // NotEqual checks that got and want are not deeply equal. -func NotEqual(t *testing.T, got, want interface{}) { +func NotEqual(t *testing.T, got, want any) { t.Helper() if reflect.DeepEqual(got, want) { t.Errorf("expected values to differ, both are %v", got) diff --git a/shared/view/view.go b/shared/view/view.go index 21e88c9..9f5d119 100644 --- a/shared/view/view.go +++ b/shared/view/view.go @@ -118,7 +118,7 @@ func (v *View) tableAsJSON(headers []string, rows [][]string) error { } // JSON renders data as formatted JSON. -func (v *View) JSON(data interface{}) error { +func (v *View) JSON(data any) error { enc := json.NewEncoder(v.Out) enc.SetIndent("", " ") return enc.Encode(data) @@ -136,7 +136,7 @@ func (v *View) Plain(rows [][]string) error { // For table format, uses headers and rows. // For JSON format, uses jsonData. // For plain format, uses rows without headers. -func (v *View) Render(headers []string, rows [][]string, jsonData interface{}) error { +func (v *View) Render(headers []string, rows [][]string, jsonData any) error { switch v.Format { case FormatJSON: return v.JSON(jsonData) @@ -150,7 +150,7 @@ func (v *View) Render(headers []string, rows [][]string, jsonData interface{}) e } // Success prints a success message with a green checkmark. -func (v *View) Success(format string, args ...interface{}) { +func (v *View) Success(format string, args ...any) { msg := fmt.Sprintf(format, args...) if v.NoColor { _, _ = fmt.Fprintln(v.Out, "✓ "+msg) @@ -160,7 +160,7 @@ func (v *View) Success(format string, args ...interface{}) { } // Error prints an error message with a red X. -func (v *View) Error(format string, args ...interface{}) { +func (v *View) Error(format string, args ...any) { msg := fmt.Sprintf(format, args...) if v.NoColor { _, _ = fmt.Fprintln(v.Err, "✗ "+msg) @@ -170,7 +170,7 @@ func (v *View) Error(format string, args ...interface{}) { } // Warning prints a warning message with a yellow warning sign. -func (v *View) Warning(format string, args ...interface{}) { +func (v *View) Warning(format string, args ...any) { msg := fmt.Sprintf(format, args...) if v.NoColor { _, _ = fmt.Fprintln(v.Err, "⚠ "+msg) @@ -180,18 +180,18 @@ func (v *View) Warning(format string, args ...interface{}) { } // Info prints an informational message. -func (v *View) Info(format string, args ...interface{}) { +func (v *View) Info(format string, args ...any) { msg := fmt.Sprintf(format, args...) _, _ = fmt.Fprintln(v.Out, msg) } // Print prints a message without newline. -func (v *View) Print(format string, args ...interface{}) { +func (v *View) Print(format string, args ...any) { _, _ = fmt.Fprintf(v.Out, format, args...) } // Println prints a message with newline. -func (v *View) Println(format string, args ...interface{}) { +func (v *View) Println(format string, args ...any) { _, _ = fmt.Fprintln(v.Out, fmt.Sprintf(format, args...)) } diff --git a/shared/view/view_test.go b/shared/view/view_test.go index e436785..96b9aee 100644 --- a/shared/view/view_test.go +++ b/shared/view/view_test.go @@ -8,6 +8,7 @@ import ( ) func TestValidFormats(t *testing.T) { + t.Parallel() formats := ValidFormats() expected := []string{"table", "json", "plain"} @@ -30,6 +31,7 @@ func TestValidFormats(t *testing.T) { } func TestValidateFormat(t *testing.T) { + t.Parallel() tests := []struct { format string wantErr bool @@ -45,6 +47,7 @@ func TestValidateFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.format, func(t *testing.T) { + t.Parallel() err := ValidateFormat(tt.format) if (err != nil) != tt.wantErr { t.Errorf("ValidateFormat(%q) error = %v, wantErr = %v", tt.format, err, tt.wantErr) @@ -54,7 +57,9 @@ func TestValidateFormat(t *testing.T) { } func TestNew(t *testing.T) { + t.Parallel() t.Run("default options", func(t *testing.T) { + t.Parallel() v := New(FormatTable, false) if v.Format != FormatTable { @@ -75,6 +80,7 @@ func TestNew(t *testing.T) { }) t.Run("with noColor", func(t *testing.T) { + t.Parallel() v := New(FormatJSON, true) if !v.NoColor { @@ -84,6 +90,7 @@ func TestNew(t *testing.T) { } func TestNewWithFormat(t *testing.T) { + t.Parallel() v := NewWithFormat("json", false) if v.Format != FormatJSON { @@ -92,6 +99,7 @@ func TestNewWithFormat(t *testing.T) { } func TestView_Table(t *testing.T) { + t.Parallel() headers := []string{"ID", "NAME", "STATUS"} rows := [][]string{ {"1", "Item One", "Active"}, @@ -99,6 +107,7 @@ func TestView_Table(t *testing.T) { } t.Run("table format", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, true) // noColor for predictable output v.SetOutput(buf) @@ -128,6 +137,7 @@ func TestView_Table(t *testing.T) { }) t.Run("json format via Table", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatJSON, false) v.SetOutput(buf) @@ -153,6 +163,7 @@ func TestView_Table(t *testing.T) { }) t.Run("plain format via Table", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatPlain, false) v.SetOutput(buf) @@ -178,6 +189,7 @@ func TestView_Table(t *testing.T) { } func TestView_JSON(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatJSON, false) v.SetOutput(buf) @@ -204,6 +216,7 @@ func TestView_JSON(t *testing.T) { } func TestView_Plain(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatPlain, false) v.SetOutput(buf) @@ -231,11 +244,13 @@ func TestView_Plain(t *testing.T) { } func TestView_Render(t *testing.T) { + t.Parallel() headers := []string{"KEY", "VALUE"} rows := [][]string{{"k1", "v1"}} jsonData := map[string]string{"key": "value"} t.Run("table format", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, true) v.SetOutput(buf) @@ -251,6 +266,7 @@ func TestView_Render(t *testing.T) { }) t.Run("json format", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatJSON, false) v.SetOutput(buf) @@ -266,6 +282,7 @@ func TestView_Render(t *testing.T) { }) t.Run("plain format", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatPlain, false) v.SetOutput(buf) @@ -286,7 +303,9 @@ func TestView_Render(t *testing.T) { } func TestView_Messages(t *testing.T) { + t.Parallel() t.Run("Success", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, true) v.SetOutput(buf) @@ -302,6 +321,7 @@ func TestView_Messages(t *testing.T) { }) t.Run("Error", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, true) v.SetError(buf) @@ -317,6 +337,7 @@ func TestView_Messages(t *testing.T) { }) t.Run("Warning", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, true) v.SetError(buf) @@ -332,6 +353,7 @@ func TestView_Messages(t *testing.T) { }) t.Run("Info", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, false) v.SetOutput(buf) @@ -344,6 +366,7 @@ func TestView_Messages(t *testing.T) { }) t.Run("Print", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, false) v.SetOutput(buf) @@ -357,6 +380,7 @@ func TestView_Messages(t *testing.T) { }) t.Run("Println", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, false) v.SetOutput(buf) @@ -371,6 +395,7 @@ func TestView_Messages(t *testing.T) { } func TestTruncate(t *testing.T) { + t.Parallel() tests := []struct { input string maxLen int @@ -388,6 +413,7 @@ func TestTruncate(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { + t.Parallel() got := Truncate(tt.input, tt.maxLen) if got != tt.want { t.Errorf("Truncate(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want) @@ -397,6 +423,7 @@ func TestTruncate(t *testing.T) { } func TestView_SetOutput(t *testing.T) { + t.Parallel() v := New(FormatTable, false) buf := &bytes.Buffer{} @@ -410,6 +437,7 @@ func TestView_SetOutput(t *testing.T) { } func TestView_SetError(t *testing.T) { + t.Parallel() v := New(FormatTable, true) buf := &bytes.Buffer{} @@ -423,6 +451,7 @@ func TestView_SetError(t *testing.T) { } func TestView_RenderList(t *testing.T) { + t.Parallel() headers := []string{"ID", "NAME"} rows := [][]string{ {"1", "First"}, @@ -430,6 +459,7 @@ func TestView_RenderList(t *testing.T) { } t.Run("table format", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, true) v.SetOutput(buf) @@ -449,6 +479,7 @@ func TestView_RenderList(t *testing.T) { }) t.Run("json format with hasMore=false", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatJSON, false) v.SetOutput(buf) @@ -478,6 +509,7 @@ func TestView_RenderList(t *testing.T) { }) t.Run("json format with hasMore=true", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatJSON, false) v.SetOutput(buf) @@ -499,7 +531,9 @@ func TestView_RenderList(t *testing.T) { } func TestView_RenderKeyValue(t *testing.T) { + t.Parallel() t.Run("table format", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, true) v.SetOutput(buf) @@ -516,6 +550,7 @@ func TestView_RenderKeyValue(t *testing.T) { }) t.Run("json format", func(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatJSON, false) v.SetOutput(buf) @@ -533,6 +568,7 @@ func TestView_RenderKeyValue(t *testing.T) { } func TestView_RenderText(t *testing.T) { + t.Parallel() buf := &bytes.Buffer{} v := New(FormatTable, false) v.SetOutput(buf) diff --git a/tools/jtk/api/automation.go b/tools/jtk/api/automation.go index ba61b3e..5f3f7b8 100644 --- a/tools/jtk/api/automation.go +++ b/tools/jtk/api/automation.go @@ -18,6 +18,10 @@ func (c *Client) ListAutomationRules(ctx context.Context) ([]AutomationRuleSumma urlStr := fmt.Sprintf("%s/rule/summary", base) for urlStr != "" { + if err := ctx.Err(); err != nil { + return nil, fmt.Errorf("listing automation rules: %w", err) + } + body, err := c.Get(ctx, urlStr) if err != nil { return nil, fmt.Errorf("listing automation rules: %w", err) diff --git a/tools/jtk/api/automation_test.go b/tools/jtk/api/automation_test.go index 84e3e31..a5f9bac 100644 --- a/tools/jtk/api/automation_test.go +++ b/tools/jtk/api/automation_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "sync/atomic" "testing" "github.com/open-cli-collective/atlassian-go/testutil" @@ -90,8 +91,28 @@ func TestAutomationBaseURL(t *testing.T) { testutil.Equal(t, baseURL, server.URL+"/gateway/api/automation/public/jira/my-cloud-id/rest/v1") } +func TestListAutomationRules_CancelledContext(t *testing.T) { + t.Parallel() + client, server := newTestClientWithServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/_edge/tenant_info" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"cloudId":"cloud-1"}`)) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{}`)) + })) + defer server.Close() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := client.ListAutomationRules(ctx) + testutil.Error(t, err) + testutil.Contains(t, err.Error(), "listing automation rules") +} + func TestListAutomationRules(t *testing.T) { - callCount := 0 client, server := newTestClientWithServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/_edge/tenant_info" { w.WriteHeader(http.StatusOK) @@ -99,7 +120,6 @@ func TestListAutomationRules(t *testing.T) { return } - callCount++ w.WriteHeader(http.StatusOK) resp := AutomationRuleSummaryResponse{ Links: automationLinks{}, @@ -550,7 +570,7 @@ func TestListAutomationRulesLegacyShape(t *testing.T) { } func TestListAutomationRulesPagination(t *testing.T) { - page := 0 + var page int32 client, server := newTestClientWithServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/_edge/tenant_info" { w.WriteHeader(http.StatusOK) @@ -558,9 +578,9 @@ func TestListAutomationRulesPagination(t *testing.T) { return } - page++ + p := atomic.AddInt32(&page, 1) w.WriteHeader(http.StatusOK) - if page == 1 { + if p == 1 { next := "http://" + r.Host + r.URL.Path + "?cursor=abc" resp := AutomationRuleSummaryResponse{ Links: automationLinks{Next: &next}, @@ -586,5 +606,5 @@ func TestListAutomationRulesPagination(t *testing.T) { testutil.Equal(t, rules[0].Name, "Rule 1") testutil.Equal(t, rules[1].Name, "Rule 2") testutil.Equal(t, rules[2].Name, "Rule 3") - testutil.Equal(t, page, 2) // Verify two pages were fetched + testutil.Equal(t, atomic.LoadInt32(&page), int32(2)) // Verify two pages were fetched } diff --git a/tools/jtk/api/client.go b/tools/jtk/api/client.go index e184922..b3b4c77 100644 --- a/tools/jtk/api/client.go +++ b/tools/jtk/api/client.go @@ -1,3 +1,4 @@ +// Package api provides a client for the Jira REST API. package api //nolint:revive // package name is intentional import ( diff --git a/tools/jtk/api/search.go b/tools/jtk/api/search.go index 83ce555..b96a8d0 100644 --- a/tools/jtk/api/search.go +++ b/tools/jtk/api/search.go @@ -87,6 +87,10 @@ func (c *Client) SearchAll(ctx context.Context, jql string, maxResults int) ([]I pageSize := 100 for { + if err := ctx.Err(); err != nil { + return nil, fmt.Errorf("searching all issues: %w", err) + } + result, err := c.Search(ctx, SearchOptions{ JQL: jql, StartAt: startAt, diff --git a/tools/jtk/api/search_test.go b/tools/jtk/api/search_test.go new file mode 100644 index 0000000..49282c2 --- /dev/null +++ b/tools/jtk/api/search_test.go @@ -0,0 +1,25 @@ +package api //nolint:revive // package name is intentional + +import ( + "context" + "net/http" + "testing" + + "github.com/open-cli-collective/atlassian-go/testutil" +) + +func TestSearchAll_CancelledContext(t *testing.T) { + t.Parallel() + client, server := newTestClientWithServer(t, func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{}`)) + }) + defer server.Close() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := client.SearchAll(ctx, "project = TEST", 100) + testutil.Error(t, err) + testutil.Contains(t, err.Error(), "searching all issues") +} diff --git a/tools/jtk/internal/cmd/configcmd/configcmd_test.go b/tools/jtk/internal/cmd/configcmd/configcmd_test.go index ca7624d..facbaf3 100644 --- a/tools/jtk/internal/cmd/configcmd/configcmd_test.go +++ b/tools/jtk/internal/cmd/configcmd/configcmd_test.go @@ -176,6 +176,7 @@ func TestNewTestCmd_NoURL(t *testing.T) { } func TestMaskToken(t *testing.T) { + t.Parallel() tests := []struct { name string token string @@ -189,6 +190,7 @@ func TestMaskToken(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() got := maskToken(tt.token) testutil.Equal(t, got, tt.want) }) diff --git a/tools/jtk/internal/cmd/initcmd/initcmd_test.go b/tools/jtk/internal/cmd/initcmd/initcmd_test.go index 09b405f..0e33bda 100644 --- a/tools/jtk/internal/cmd/initcmd/initcmd_test.go +++ b/tools/jtk/internal/cmd/initcmd/initcmd_test.go @@ -28,6 +28,7 @@ func TestConfig_GetDefaultProject_NoConfig(t *testing.T) { } func TestConfig_DefaultProject_Struct(t *testing.T) { + t.Parallel() // Test that the Config struct has the DefaultProject field cfg := &config.Config{ URL: "https://test.atlassian.net", diff --git a/tools/jtk/internal/config/config_test.go b/tools/jtk/internal/config/config_test.go index bab49b6..9560ecd 100644 --- a/tools/jtk/internal/config/config_test.go +++ b/tools/jtk/internal/config/config_test.go @@ -180,6 +180,7 @@ func TestGetURL_URLTakesPrecedence(t *testing.T) { } func TestNormalizeURL(t *testing.T) { + t.Parallel() tests := []struct { input string want string @@ -194,6 +195,7 @@ func TestNormalizeURL(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { + t.Parallel() testutil.Equal(t, url.NormalizeURL(tt.input), tt.want) }) } @@ -316,6 +318,7 @@ func TestIsConfigured_LegacyEnvOnly(t *testing.T) { } func TestPath(t *testing.T) { + t.Parallel() path := Path() testutil.Contains(t, path, configDirName) testutil.Contains(t, path, configFileName) From 56d3e35606e0473aeeac1156df61a91831e513ad Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 19 Feb 2026 07:50:44 -0500 Subject: [PATCH 14/14] refactor: align new dashboards, links, and text packages with STANDARDS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring the dashboards (#165), links (#164), and text packages — added to main after the branch diverged — into alignment with the project coding standards applied by earlier commits in this branch: - Replace c.get/post/delete helpers with context-aware c.Get/Post/Delete - Convert testify assertions to shared/testutil helpers - Add _ = to unhandled json.NewEncoder errors (gosec G104) - Rename unused cmd/r parameters to _ (revive unused-parameter) - Add package doc comments (revive package-comments) - Wire context.Context through changeIssueType --- tools/jtk/api/dashboards.go | 55 ++++++------ tools/jtk/api/dashboards_test.go | 87 +++++++++---------- tools/jtk/api/links.go | 13 +-- tools/jtk/api/links_test.go | 79 +++++++++-------- .../jtk/internal/cmd/dashboards/dashboards.go | 13 +-- .../cmd/dashboards/dashboards_test.go | 81 +++++++++-------- tools/jtk/internal/cmd/issues/create_test.go | 24 ++--- tools/jtk/internal/cmd/issues/update.go | 12 +-- tools/jtk/internal/cmd/issues/update_test.go | 10 +-- tools/jtk/internal/cmd/links/links.go | 9 +- tools/jtk/internal/cmd/links/links_test.go | 87 +++++++++---------- tools/jtk/internal/text/escapes.go | 1 + 12 files changed, 236 insertions(+), 235 deletions(-) diff --git a/tools/jtk/api/dashboards.go b/tools/jtk/api/dashboards.go index c0cb8e4..4ad18ef 100644 --- a/tools/jtk/api/dashboards.go +++ b/tools/jtk/api/dashboards.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "fmt" "net/url" @@ -27,13 +28,13 @@ type SharePerm struct { // DashboardGadget represents a gadget on a dashboard type DashboardGadget struct { - ID int `json:"id"` - Title string `json:"title"` - ModuleID string `json:"moduleKey,omitempty"` - URI string `json:"uri,omitempty"` - Color string `json:"color,omitempty"` - Position DashboardGadgetPos `json:"position,omitempty"` - Props map[string]interface{} `json:"properties,omitempty"` + ID int `json:"id"` + Title string `json:"title"` + ModuleID string `json:"moduleKey,omitempty"` + URI string `json:"uri,omitempty"` + Color string `json:"color,omitempty"` + Position DashboardGadgetPos `json:"position,omitempty"` + Props map[string]any `json:"properties,omitempty"` } // DashboardGadgetPos represents the position of a gadget on a dashboard @@ -63,6 +64,14 @@ type CreateDashboardRequest struct { SharePermissions []SharePerm `json:"sharePermissions"` } +// DashboardSearchResponse represents the response from dashboard search +type DashboardSearchResponse struct { + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` + Total int `json:"total"` + Values []Dashboard `json:"values"` +} + // GetDashboards returns a paginated list of dashboards func (c *Client) GetDashboards(startAt, maxResults int) (*DashboardsResponse, error) { params := map[string]string{} @@ -75,14 +84,14 @@ func (c *Client) GetDashboards(startAt, maxResults int) (*DashboardsResponse, er urlStr := buildURL(fmt.Sprintf("%s/dashboard", c.BaseURL), params) - body, err := c.get(urlStr) + body, err := c.Get(context.Background(), urlStr) if err != nil { return nil, err } var result DashboardsResponse if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("failed to parse dashboards: %w", err) + return nil, fmt.Errorf("parsing dashboards: %w", err) } return &result, nil @@ -100,27 +109,19 @@ func (c *Client) SearchDashboards(name string, maxResults int) (*DashboardSearch urlStr := buildURL(fmt.Sprintf("%s/dashboard/search", c.BaseURL), params) - body, err := c.get(urlStr) + body, err := c.Get(context.Background(), urlStr) if err != nil { return nil, err } var result DashboardSearchResponse if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("failed to parse dashboard search: %w", err) + return nil, fmt.Errorf("parsing dashboard search: %w", err) } return &result, nil } -// DashboardSearchResponse represents the response from dashboard search -type DashboardSearchResponse struct { - StartAt int `json:"startAt"` - MaxResults int `json:"maxResults"` - Total int `json:"total"` - Values []Dashboard `json:"values"` -} - // GetDashboard returns a dashboard by ID func (c *Client) GetDashboard(dashboardID string) (*Dashboard, error) { if dashboardID == "" { @@ -129,14 +130,14 @@ func (c *Client) GetDashboard(dashboardID string) (*Dashboard, error) { urlStr := fmt.Sprintf("%s/dashboard/%s", c.BaseURL, url.PathEscape(dashboardID)) - body, err := c.get(urlStr) + body, err := c.Get(context.Background(), urlStr) if err != nil { return nil, err } var dash Dashboard if err := json.Unmarshal(body, &dash); err != nil { - return nil, fmt.Errorf("failed to parse dashboard: %w", err) + return nil, fmt.Errorf("parsing dashboard: %w", err) } return &dash, nil @@ -146,14 +147,14 @@ func (c *Client) GetDashboard(dashboardID string) (*Dashboard, error) { func (c *Client) CreateDashboard(req CreateDashboardRequest) (*Dashboard, error) { urlStr := fmt.Sprintf("%s/dashboard", c.BaseURL) - body, err := c.post(urlStr, req) + body, err := c.Post(context.Background(), urlStr, req) if err != nil { return nil, err } var dash Dashboard if err := json.Unmarshal(body, &dash); err != nil { - return nil, fmt.Errorf("failed to parse dashboard: %w", err) + return nil, fmt.Errorf("parsing dashboard: %w", err) } return &dash, nil @@ -166,7 +167,7 @@ func (c *Client) DeleteDashboard(dashboardID string) error { } urlStr := fmt.Sprintf("%s/dashboard/%s", c.BaseURL, url.PathEscape(dashboardID)) - _, err := c.delete(urlStr) + _, err := c.Delete(context.Background(), urlStr) return err } @@ -178,14 +179,14 @@ func (c *Client) GetDashboardGadgets(dashboardID string) (*DashboardGadgetsRespo urlStr := fmt.Sprintf("%s/dashboard/%s/gadget", c.BaseURL, url.PathEscape(dashboardID)) - body, err := c.get(urlStr) + body, err := c.Get(context.Background(), urlStr) if err != nil { return nil, err } var result DashboardGadgetsResponse if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("failed to parse gadgets: %w", err) + return nil, fmt.Errorf("parsing gadgets: %w", err) } return &result, nil @@ -198,6 +199,6 @@ func (c *Client) RemoveDashboardGadget(dashboardID string, gadgetID int) error { } urlStr := fmt.Sprintf("%s/dashboard/%s/gadget/%d", c.BaseURL, url.PathEscape(dashboardID), gadgetID) - _, err := c.delete(urlStr) + _, err := c.Delete(context.Background(), urlStr) return err } diff --git a/tools/jtk/api/dashboards_test.go b/tools/jtk/api/dashboards_test.go index 3215a31..428f25a 100644 --- a/tools/jtk/api/dashboards_test.go +++ b/tools/jtk/api/dashboards_test.go @@ -7,14 +7,13 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestGetDashboards(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/dashboard", r.URL.Path) - json.NewEncoder(w).Encode(DashboardsResponse{ + testutil.Equal(t, r.URL.Path, "/rest/api/3/dashboard") + _ = json.NewEncoder(w).Encode(DashboardsResponse{ Total: 1, Dashboards: []Dashboard{ {ID: "10001", Name: "My Dashboard"}, @@ -24,19 +23,19 @@ func TestGetDashboards(t *testing.T) { defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) result, err := client.GetDashboards(0, 50) - require.NoError(t, err) - require.Len(t, result.Dashboards, 1) - assert.Equal(t, "My Dashboard", result.Dashboards[0].Name) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Dashboards, 1) + testutil.Equal(t, result.Dashboards[0].Name, "My Dashboard") } func TestSearchDashboards(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/dashboard/search", r.URL.Path) - assert.Equal(t, "Sprint", r.URL.Query().Get("dashboardName")) - json.NewEncoder(w).Encode(DashboardSearchResponse{ + testutil.Equal(t, r.URL.Path, "/rest/api/3/dashboard/search") + testutil.Equal(t, r.URL.Query().Get("dashboardName"), "Sprint") + _ = json.NewEncoder(w).Encode(DashboardSearchResponse{ Total: 1, Values: []Dashboard{ {ID: "10002", Name: "Sprint Board"}, @@ -46,18 +45,18 @@ func TestSearchDashboards(t *testing.T) { defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) result, err := client.SearchDashboards("Sprint", 50) - require.NoError(t, err) - require.Len(t, result.Values, 1) - assert.Equal(t, "Sprint Board", result.Values[0].Name) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Values, 1) + testutil.Equal(t, result.Values[0].Name, "Sprint Board") } func TestGetDashboard(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/dashboard/10001", r.URL.Path) - json.NewEncoder(w).Encode(Dashboard{ + testutil.Equal(t, r.URL.Path, "/rest/api/3/dashboard/10001") + _ = json.NewEncoder(w).Encode(Dashboard{ ID: "10001", Name: "My Dashboard", }) @@ -65,69 +64,69 @@ func TestGetDashboard(t *testing.T) { defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) dash, err := client.GetDashboard("10001") - require.NoError(t, err) - assert.Equal(t, "My Dashboard", dash.Name) + testutil.RequireNoError(t, err) + testutil.Equal(t, dash.Name, "My Dashboard") } func TestGetDashboard_EmptyID(t *testing.T) { _, err := (&Client{}).GetDashboard("") - assert.Error(t, err) + testutil.Error(t, err) } func TestCreateDashboard(t *testing.T) { var capturedBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "POST", r.Method) + testutil.Equal(t, r.Method, "POST") capturedBody, _ = io.ReadAll(r.Body) w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(Dashboard{ID: "10099", Name: "New Board"}) + _ = json.NewEncoder(w).Encode(Dashboard{ID: "10099", Name: "New Board"}) })) defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) dash, err := client.CreateDashboard(CreateDashboardRequest{ Name: "New Board", EditPermissions: []SharePerm{{Type: "global"}}, SharePermissions: []SharePerm{{Type: "global"}}, }) - require.NoError(t, err) - assert.Equal(t, "10099", dash.ID) - assert.Equal(t, "New Board", dash.Name) + testutil.RequireNoError(t, err) + testutil.Equal(t, dash.ID, "10099") + testutil.Equal(t, dash.Name, "New Board") var req CreateDashboardRequest err = json.Unmarshal(capturedBody, &req) - require.NoError(t, err) - assert.Equal(t, "New Board", req.Name) + testutil.RequireNoError(t, err) + testutil.Equal(t, req.Name, "New Board") } func TestDeleteDashboard(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/dashboard/10001", r.URL.Path) - assert.Equal(t, "DELETE", r.Method) + testutil.Equal(t, r.URL.Path, "/rest/api/3/dashboard/10001") + testutil.Equal(t, r.Method, "DELETE") w.WriteHeader(http.StatusNoContent) })) defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) err = client.DeleteDashboard("10001") - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestDeleteDashboard_EmptyID(t *testing.T) { - assert.Error(t, (&Client{}).DeleteDashboard("")) + testutil.Error(t, (&Client{}).DeleteDashboard("")) } func TestGetDashboardGadgets(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/dashboard/10001/gadget", r.URL.Path) - json.NewEncoder(w).Encode(DashboardGadgetsResponse{ + testutil.Equal(t, r.URL.Path, "/rest/api/3/dashboard/10001/gadget") + _ = json.NewEncoder(w).Encode(DashboardGadgetsResponse{ Gadgets: []DashboardGadget{ {ID: 1, Title: "Filter Results"}, {ID: 2, Title: "Pie Chart"}, @@ -137,25 +136,25 @@ func TestGetDashboardGadgets(t *testing.T) { defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) result, err := client.GetDashboardGadgets("10001") - require.NoError(t, err) - require.Len(t, result.Gadgets, 2) - assert.Equal(t, "Filter Results", result.Gadgets[0].Title) + testutil.RequireNoError(t, err) + testutil.Len(t, result.Gadgets, 2) + testutil.Equal(t, result.Gadgets[0].Title, "Filter Results") } func TestRemoveDashboardGadget(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/dashboard/10001/gadget/42", r.URL.Path) - assert.Equal(t, "DELETE", r.Method) + testutil.Equal(t, r.URL.Path, "/rest/api/3/dashboard/10001/gadget/42") + testutil.Equal(t, r.Method, "DELETE") w.WriteHeader(http.StatusNoContent) })) defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) err = client.RemoveDashboardGadget("10001", 42) - require.NoError(t, err) + testutil.RequireNoError(t, err) } diff --git a/tools/jtk/api/links.go b/tools/jtk/api/links.go index c0ca5f1..ad9a7b0 100644 --- a/tools/jtk/api/links.go +++ b/tools/jtk/api/links.go @@ -1,6 +1,7 @@ package api import ( + "context" "encoding/json" "fmt" "net/url" @@ -62,7 +63,7 @@ func (c *Client) GetIssueLinks(issueKey string) ([]IssueLink, error) { map[string]string{"fields": "issuelinks"}, ) - body, err := c.get(urlStr) + body, err := c.Get(context.Background(), urlStr) if err != nil { return nil, err } @@ -73,7 +74,7 @@ func (c *Client) GetIssueLinks(issueKey string) ([]IssueLink, error) { } `json:"fields"` } if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("failed to parse issue links: %w", err) + return nil, fmt.Errorf("parsing issue links: %w", err) } return result.Fields.IssueLinks, nil @@ -95,7 +96,7 @@ func (c *Client) CreateIssueLink(outwardKey, inwardKey, linkTypeName string) err InwardIssue: IssueRef{Key: inwardKey}, } - _, err := c.post(urlStr, req) + _, err := c.Post(context.Background(), urlStr, req) return err } @@ -106,7 +107,7 @@ func (c *Client) DeleteIssueLink(linkID string) error { } urlStr := fmt.Sprintf("%s/issueLink/%s", c.BaseURL, url.PathEscape(linkID)) - _, err := c.delete(urlStr) + _, err := c.Delete(context.Background(), urlStr) return err } @@ -114,7 +115,7 @@ func (c *Client) DeleteIssueLink(linkID string) error { func (c *Client) GetIssueLinkTypes() ([]IssueLinkType, error) { urlStr := fmt.Sprintf("%s/issueLinkType", c.BaseURL) - body, err := c.get(urlStr) + body, err := c.Get(context.Background(), urlStr) if err != nil { return nil, err } @@ -123,7 +124,7 @@ func (c *Client) GetIssueLinkTypes() ([]IssueLinkType, error) { IssueLinkTypes []IssueLinkType `json:"issueLinkTypes"` } if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("failed to parse link types: %w", err) + return nil, fmt.Errorf("parsing link types: %w", err) } return result.IssueLinkTypes, nil diff --git a/tools/jtk/api/links_test.go b/tools/jtk/api/links_test.go index e4d94e2..108cfce 100644 --- a/tools/jtk/api/links_test.go +++ b/tools/jtk/api/links_test.go @@ -7,24 +7,23 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" ) func TestGetIssueLinks(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/issue/PROJ-123", r.URL.Path) - assert.Equal(t, "issuelinks", r.URL.Query().Get("fields")) + testutil.Equal(t, r.URL.Path, "/rest/api/3/issue/PROJ-123") + testutil.Equal(t, r.URL.Query().Get("fields"), "issuelinks") - json.NewEncoder(w).Encode(map[string]interface{}{ - "fields": map[string]interface{}{ - "issuelinks": []map[string]interface{}{ + _ = json.NewEncoder(w).Encode(map[string]any{ + "fields": map[string]any{ + "issuelinks": []map[string]any{ { "id": "10001", "type": map[string]string{"id": "1", "name": "Blocks", "inward": "is blocked by", "outward": "blocks"}, - "outwardIssue": map[string]interface{}{ + "outwardIssue": map[string]any{ "key": "PROJ-456", - "fields": map[string]interface{}{ + "fields": map[string]any{ "summary": "Other issue", }, }, @@ -36,75 +35,75 @@ func TestGetIssueLinks(t *testing.T) { defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) links, err := client.GetIssueLinks("PROJ-123") - require.NoError(t, err) - require.Len(t, links, 1) - assert.Equal(t, "10001", links[0].ID) - assert.Equal(t, "Blocks", links[0].Type.Name) - require.NotNil(t, links[0].OutwardIssue) - assert.Equal(t, "PROJ-456", links[0].OutwardIssue.Key) + testutil.RequireNoError(t, err) + testutil.Len(t, links, 1) + testutil.Equal(t, links[0].ID, "10001") + testutil.Equal(t, links[0].Type.Name, "Blocks") + testutil.NotNil(t, links[0].OutwardIssue) + testutil.Equal(t, links[0].OutwardIssue.Key, "PROJ-456") } func TestGetIssueLinks_EmptyKey(t *testing.T) { _, err := (&Client{}).GetIssueLinks("") - assert.Equal(t, ErrIssueKeyRequired, err) + testutil.Equal(t, err, ErrIssueKeyRequired) } func TestCreateIssueLink(t *testing.T) { var capturedBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/issueLink", r.URL.Path) - assert.Equal(t, "POST", r.Method) + testutil.Equal(t, r.URL.Path, "/rest/api/3/issueLink") + testutil.Equal(t, r.Method, "POST") capturedBody, _ = io.ReadAll(r.Body) w.WriteHeader(http.StatusCreated) })) defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) err = client.CreateIssueLink("PROJ-123", "PROJ-456", "Blocks") - require.NoError(t, err) + testutil.RequireNoError(t, err) var req CreateIssueLinkRequest err = json.Unmarshal(capturedBody, &req) - require.NoError(t, err) - assert.Equal(t, "Blocks", req.Type.Name) - assert.Equal(t, "PROJ-123", req.OutwardIssue.Key) - assert.Equal(t, "PROJ-456", req.InwardIssue.Key) + testutil.RequireNoError(t, err) + testutil.Equal(t, req.Type.Name, "Blocks") + testutil.Equal(t, req.OutwardIssue.Key, "PROJ-123") + testutil.Equal(t, req.InwardIssue.Key, "PROJ-456") } func TestCreateIssueLink_EmptyKeys(t *testing.T) { - assert.Error(t, (&Client{}).CreateIssueLink("", "B", "t")) - assert.Error(t, (&Client{}).CreateIssueLink("A", "", "t")) - assert.Error(t, (&Client{}).CreateIssueLink("A", "B", "")) + testutil.Error(t, (&Client{}).CreateIssueLink("", "B", "t")) + testutil.Error(t, (&Client{}).CreateIssueLink("A", "", "t")) + testutil.Error(t, (&Client{}).CreateIssueLink("A", "B", "")) } func TestDeleteIssueLink(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/issueLink/10001", r.URL.Path) - assert.Equal(t, "DELETE", r.Method) + testutil.Equal(t, r.URL.Path, "/rest/api/3/issueLink/10001") + testutil.Equal(t, r.Method, "DELETE") w.WriteHeader(http.StatusNoContent) })) defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) err = client.DeleteIssueLink("10001") - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestDeleteIssueLink_EmptyID(t *testing.T) { - assert.Error(t, (&Client{}).DeleteIssueLink("")) + testutil.Error(t, (&Client{}).DeleteIssueLink("")) } func TestGetIssueLinkTypes(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/issueLinkType", r.URL.Path) - json.NewEncoder(w).Encode(map[string]interface{}{ + testutil.Equal(t, r.URL.Path, "/rest/api/3/issueLinkType") + _ = json.NewEncoder(w).Encode(map[string]any{ "issueLinkTypes": []map[string]string{ {"id": "1", "name": "Blocks", "inward": "is blocked by", "outward": "blocks"}, {"id": "2", "name": "Relates", "inward": "relates to", "outward": "relates to"}, @@ -114,11 +113,11 @@ func TestGetIssueLinkTypes(t *testing.T) { defer server.Close() client, err := New(ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) types, err := client.GetIssueLinkTypes() - require.NoError(t, err) - require.Len(t, types, 2) - assert.Equal(t, "Blocks", types[0].Name) - assert.Equal(t, "Relates", types[1].Name) + testutil.RequireNoError(t, err) + testutil.Len(t, types, 2) + testutil.Equal(t, types[0].Name, "Blocks") + testutil.Equal(t, types[1].Name, "Relates") } diff --git a/tools/jtk/internal/cmd/dashboards/dashboards.go b/tools/jtk/internal/cmd/dashboards/dashboards.go index 34b06c2..acc786f 100644 --- a/tools/jtk/internal/cmd/dashboards/dashboards.go +++ b/tools/jtk/internal/cmd/dashboards/dashboards.go @@ -1,3 +1,4 @@ +// Package dashboards provides CLI commands for managing Jira dashboards. package dashboards import ( @@ -39,7 +40,7 @@ func newListCmd(opts *root.Options) *cobra.Command { Example: ` jtk dashboards list jtk dashboards list --search "Sprint" jtk dashboards list --max 10`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { return runList(opts, search, maxResults) }, } @@ -101,7 +102,7 @@ func newGetCmd(opts *root.Options) *cobra.Command { Example: ` jtk dashboards get 10001 jtk dashboards get 10001 -o json`, Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { return runGet(opts, args[0]) }, } @@ -180,7 +181,7 @@ func newCreateCmd(opts *root.Options) *cobra.Command { Long: "Create a new Jira dashboard.", Example: ` jtk dashboards create --name "My Dashboard" jtk dashboards create --name "Sprint Board" --description "Sprint tracking"`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { return runCreate(opts, name, description) }, } @@ -231,7 +232,7 @@ func newDeleteCmd(opts *root.Options) *cobra.Command { Long: "Delete a Jira dashboard by its ID.", Example: ` jtk dashboards delete 10001`, Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { return runDelete(opts, args[0]) }, } @@ -280,7 +281,7 @@ func newGadgetsListCmd(opts *root.Options) *cobra.Command { Example: ` jtk dashboards gadgets list 10001 jtk dashboards gadgets list 10001 -o json`, Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { return runGadgetsList(opts, args[0]) }, } @@ -333,7 +334,7 @@ func newGadgetsRemoveCmd(opts *root.Options) *cobra.Command { Long: "Remove a gadget from a dashboard by its ID.", Example: ` jtk dashboards gadgets remove 10001 42`, Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { gadgetID, err := strconv.Atoi(args[1]) if err != nil { return fmt.Errorf("invalid gadget ID: %s", args[1]) diff --git a/tools/jtk/internal/cmd/dashboards/dashboards_test.go b/tools/jtk/internal/cmd/dashboards/dashboards_test.go index a0be12d..803b550 100644 --- a/tools/jtk/internal/cmd/dashboards/dashboards_test.go +++ b/tools/jtk/internal/cmd/dashboards/dashboards_test.go @@ -8,16 +8,15 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/jira-ticket-cli/api" "github.com/open-cli-collective/jira-ticket-cli/internal/cmd/root" ) func TestRunList(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - json.NewEncoder(w).Encode(api.DashboardsResponse{ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(api.DashboardsResponse{ Total: 1, Dashboards: []api.Dashboard{ {ID: "10001", Name: "Sprint Board", Owner: &api.User{DisplayName: "Alice"}}, @@ -27,21 +26,21 @@ func TestRunList(t *testing.T) { defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runList(opts, "", 50) - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Sprint Board") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "Sprint Board") } func TestRunList_Search(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "Sprint", r.URL.Query().Get("dashboardName")) - json.NewEncoder(w).Encode(api.DashboardSearchResponse{ + testutil.Equal(t, r.URL.Query().Get("dashboardName"), "Sprint") + _ = json.NewEncoder(w).Encode(api.DashboardSearchResponse{ Total: 1, Values: []api.Dashboard{ {ID: "10002", Name: "Sprint Board"}, @@ -51,27 +50,27 @@ func TestRunList_Search(t *testing.T) { defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runList(opts, "Sprint", 50) - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Sprint Board") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "Sprint Board") } func TestRunGet(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/rest/api/3/dashboard/10001": - json.NewEncoder(w).Encode(api.Dashboard{ + _ = json.NewEncoder(w).Encode(api.Dashboard{ ID: "10001", Name: "My Dashboard", }) case "/rest/api/3/dashboard/10001/gadget": - json.NewEncoder(w).Encode(api.DashboardGadgetsResponse{ + _ = json.NewEncoder(w).Encode(api.DashboardGadgetsResponse{ Gadgets: []api.DashboardGadget{ {ID: 1, Title: "Filter Results"}, }, @@ -83,67 +82,67 @@ func TestRunGet(t *testing.T) { defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runGet(opts, "10001") - require.NoError(t, err) - assert.Contains(t, stdout.String(), "My Dashboard") - assert.Contains(t, stdout.String(), "Filter Results") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "My Dashboard") + testutil.Contains(t, stdout.String(), "Filter Results") } func TestRunCreate(t *testing.T) { var capturedBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { capturedBody, _ = io.ReadAll(r.Body) - json.NewEncoder(w).Encode(api.Dashboard{ID: "10099", Name: "New Board"}) + _ = json.NewEncoder(w).Encode(api.Dashboard{ID: "10099", Name: "New Board"}) })) defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runCreate(opts, "New Board", "Description") - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Created") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "Created") var req api.CreateDashboardRequest err = json.Unmarshal(capturedBody, &req) - require.NoError(t, err) - assert.Equal(t, "New Board", req.Name) - assert.Equal(t, "Description", req.Description) + testutil.RequireNoError(t, err) + testutil.Equal(t, req.Name, "New Board") + testutil.Equal(t, req.Description, "Description") } func TestRunDelete(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/dashboard/10001", r.URL.Path) - assert.Equal(t, "DELETE", r.Method) + testutil.Equal(t, r.URL.Path, "/rest/api/3/dashboard/10001") + testutil.Equal(t, r.Method, "DELETE") w.WriteHeader(http.StatusNoContent) })) defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runDelete(opts, "10001") - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Deleted") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "Deleted") } func TestRunGadgetsList(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - json.NewEncoder(w).Encode(api.DashboardGadgetsResponse{ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(api.DashboardGadgetsResponse{ Gadgets: []api.DashboardGadget{ {ID: 1, Title: "Filter Results", ModuleID: "com.atlassian.jira.gadgets:filter-results-gadget"}, {ID: 2, Title: "Pie Chart", ModuleID: "com.atlassian.jira.gadgets:pie-chart-gadget"}, @@ -153,34 +152,34 @@ func TestRunGadgetsList(t *testing.T) { defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runGadgetsList(opts, "10001") - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Filter Results") - assert.Contains(t, stdout.String(), "Pie Chart") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "Filter Results") + testutil.Contains(t, stdout.String(), "Pie Chart") } func TestRunGadgetsRemove(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/dashboard/10001/gadget/42", r.URL.Path) - assert.Equal(t, "DELETE", r.Method) + testutil.Equal(t, r.URL.Path, "/rest/api/3/dashboard/10001/gadget/42") + testutil.Equal(t, r.Method, "DELETE") w.WriteHeader(http.StatusNoContent) })) defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runGadgetsRemove(opts, "10001", 42) - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Removed") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "Removed") } diff --git a/tools/jtk/internal/cmd/issues/create_test.go b/tools/jtk/internal/cmd/issues/create_test.go index ad0949d..223e832 100644 --- a/tools/jtk/internal/cmd/issues/create_test.go +++ b/tools/jtk/internal/cmd/issues/create_test.go @@ -484,7 +484,7 @@ func TestRunCreate_DescriptionEscapeSequences(t *testing.T) { if r.URL.Path == "/rest/api/3/issue" && r.Method == "POST" { capturedBody, _ = io.ReadAll(r.Body) w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(api.Issue{Key: "TEST-10", ID: "10010"}) + _ = json.NewEncoder(w).Encode(api.Issue{Key: "TEST-10", ID: "10010"}) return } w.WriteHeader(http.StatusNotFound) @@ -496,7 +496,7 @@ func TestRunCreate_DescriptionEscapeSequences(t *testing.T) { Email: "test@example.com", APIToken: "token", }) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{ @@ -508,22 +508,22 @@ func TestRunCreate_DescriptionEscapeSequences(t *testing.T) { // Simulate what the shell passes when user types: --description "First paragraph.\n\nSecond paragraph." // The shell delivers literal backslash-n, not actual newlines. - err = runCreate(opts, "PROJ", "Task", "Test", `First paragraph.\n\nSecond paragraph.`, "", "", nil) - require.NoError(t, err) + err = runCreate(context.Background(), opts, "PROJ", "Task", "Test", `First paragraph.\n\nSecond paragraph.`, "", "", nil) + testutil.RequireNoError(t, err) - require.NotEmpty(t, capturedBody) - var reqBody map[string]interface{} + testutil.NotEmpty(t, capturedBody) + var reqBody map[string]any err = json.Unmarshal(capturedBody, &reqBody) - require.NoError(t, err) + testutil.RequireNoError(t, err) - fields := reqBody["fields"].(map[string]interface{}) - desc := fields["description"].(map[string]interface{}) - assert.Equal(t, "doc", desc["type"]) + fields := reqBody["fields"].(map[string]any) + desc := fields["description"].(map[string]any) + testutil.Equal(t, desc["type"], "doc") // With escape interpretation, the description should produce multiple paragraphs // (not a single paragraph with literal \n text) - content := desc["content"].([]interface{}) - assert.GreaterOrEqual(t, len(content), 2, "escaped newlines should produce multiple ADF nodes, not one paragraph with literal \\n") + content := desc["content"].([]any) + testutil.GreaterOrEqual(t, len(content), 2) } func TestRunCreate_WithoutAssignee(t *testing.T) { diff --git a/tools/jtk/internal/cmd/issues/update.go b/tools/jtk/internal/cmd/issues/update.go index 4ae09de..85c2cd1 100644 --- a/tools/jtk/internal/cmd/issues/update.go +++ b/tools/jtk/internal/cmd/issues/update.go @@ -76,7 +76,7 @@ func runUpdate(ctx context.Context, opts *root.Options, issueKey, summary, descr // Handle type change via the move API if issueType != "" { - if err := changeIssueType(client, v, issueKey, issueType); err != nil { + if err := changeIssueType(ctx, client, v, issueKey, issueType); err != nil { return err } } @@ -152,12 +152,12 @@ func runUpdate(ctx context.Context, opts *root.Options, issueKey, summary, descr return nil } -func changeIssueType(client *api.Client, v interface { +func changeIssueType(ctx context.Context, client *api.Client, v interface { Info(string, ...any) Success(string, ...any) }, issueKey, targetTypeName string) error { // Get the issue to find its project - issue, err := client.GetIssue(issueKey) + issue, err := client.GetIssue(ctx, issueKey) if err != nil { return fmt.Errorf("failed to get issue: %w", err) } @@ -174,7 +174,7 @@ func changeIssueType(client *api.Client, v interface { } // Get available issue types in the project - issueTypes, err := client.GetProjectIssueTypes(projectKey) + issueTypes, err := client.GetProjectIssueTypes(ctx, projectKey) if err != nil { return fmt.Errorf("failed to get project issue types: %w", err) } @@ -202,7 +202,7 @@ func changeIssueType(client *api.Client, v interface { // Use the move API to change the type within the same project req := api.BuildMoveRequest([]string{issueKey}, projectKey, targetIssueType.ID, false) - resp, err := client.MoveIssues(req) + resp, err := client.MoveIssues(ctx, req) if err != nil { if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") { return fmt.Errorf("type change failed - this feature requires Jira Cloud") @@ -212,7 +212,7 @@ func changeIssueType(client *api.Client, v interface { // Wait for completion for { - status, err := client.GetMoveTaskStatus(resp.TaskID) + status, err := client.GetMoveTaskStatus(ctx, resp.TaskID) if err != nil { return fmt.Errorf("failed to get task status: %w", err) } diff --git a/tools/jtk/internal/cmd/issues/update_test.go b/tools/jtk/internal/cmd/issues/update_test.go index ec350ca..e35579e 100644 --- a/tools/jtk/internal/cmd/issues/update_test.go +++ b/tools/jtk/internal/cmd/issues/update_test.go @@ -108,7 +108,7 @@ func TestRunUpdate_TypeChange(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/rest/api/3/issue/PROJ-123" && r.Method == "GET": - json.NewEncoder(w).Encode(api.Issue{ + _ = json.NewEncoder(w).Encode(api.Issue{ Key: "PROJ-123", ID: "10001", Fields: api.IssueFields{ @@ -117,7 +117,7 @@ func TestRunUpdate_TypeChange(t *testing.T) { }, }) case r.URL.Path == "/rest/api/3/project/PROJ" && r.Method == "GET": - json.NewEncoder(w).Encode(struct { + _ = json.NewEncoder(w).Encode(struct { IssueTypes []api.IssueType `json:"issueTypes"` }{ IssueTypes: []api.IssueType{ @@ -129,9 +129,9 @@ func TestRunUpdate_TypeChange(t *testing.T) { case r.URL.Path == "/rest/api/3/bulk/issues/move" && r.Method == "POST": moveBody, _ = io.ReadAll(r.Body) moveCompleted = true - json.NewEncoder(w).Encode(api.MoveIssuesResponse{TaskID: "task-123"}) + _ = json.NewEncoder(w).Encode(api.MoveIssuesResponse{TaskID: "task-123"}) case r.URL.Path == "/rest/api/3/bulk/queue/task-123" && r.Method == "GET": - json.NewEncoder(w).Encode(api.MoveTaskStatus{ + _ = json.NewEncoder(w).Encode(api.MoveTaskStatus{ TaskID: "task-123", Status: "COMPLETE", Progress: 100, @@ -176,7 +176,7 @@ func TestRunUpdate_TypeChange(t *testing.T) { func TestRunUpdate_TypeAlreadyCorrect(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/rest/api/3/issue/PROJ-123" && r.Method == "GET" { - json.NewEncoder(w).Encode(api.Issue{ + _ = json.NewEncoder(w).Encode(api.Issue{ Key: "PROJ-123", ID: "10001", Fields: api.IssueFields{ diff --git a/tools/jtk/internal/cmd/links/links.go b/tools/jtk/internal/cmd/links/links.go index 1488714..0aa7cee 100644 --- a/tools/jtk/internal/cmd/links/links.go +++ b/tools/jtk/internal/cmd/links/links.go @@ -1,3 +1,4 @@ +// Package links provides CLI commands for managing Jira issue links. package links import ( @@ -35,7 +36,7 @@ func newListCmd(opts *root.Options) *cobra.Command { Example: ` jtk links list PROJ-123 jtk links list PROJ-123 -o json`, Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { return runList(opts, args[0]) }, } @@ -114,7 +115,7 @@ For example, "jtk links create A B --type Blocks" means "A blocks B".`, # A is cloned by B jtk links create PROJ-123 PROJ-456 --type Cloners`, Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { return runCreate(opts, args[0], args[1], linkType) }, } @@ -181,7 +182,7 @@ func newDeleteCmd(opts *root.Options) *cobra.Command { Example: ` jtk links delete 10001 jtk links list PROJ-123 # find link IDs first`, Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { return runDelete(opts, args[0]) }, } @@ -216,7 +217,7 @@ func newTypesCmd(opts *root.Options) *cobra.Command { Long: "List all available issue link types in the Jira instance.", Example: ` jtk links types jtk links types -o json`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { return runTypes(opts) }, } diff --git a/tools/jtk/internal/cmd/links/links_test.go b/tools/jtk/internal/cmd/links/links_test.go index da86680..fce012e 100644 --- a/tools/jtk/internal/cmd/links/links_test.go +++ b/tools/jtk/internal/cmd/links/links_test.go @@ -8,8 +8,7 @@ import ( "net/http/httptest" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/open-cli-collective/atlassian-go/testutil" "github.com/open-cli-collective/jira-ticket-cli/api" "github.com/open-cli-collective/jira-ticket-cli/internal/cmd/root" @@ -19,19 +18,19 @@ func TestNewListCmd(t *testing.T) { opts := &root.Options{} cmd := newListCmd(opts) - assert.Equal(t, "list ", cmd.Use) - assert.Equal(t, "List links on an issue", cmd.Short) + testutil.Equal(t, cmd.Use, "list ") + testutil.Equal(t, cmd.Short, "List links on an issue") } func TestRunList(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - json.NewEncoder(w).Encode(map[string]interface{}{ - "fields": map[string]interface{}{ - "issuelinks": []map[string]interface{}{ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{ + "fields": map[string]any{ + "issuelinks": []map[string]any{ { "id": "10001", "type": map[string]string{"id": "1", "name": "Blocks", "inward": "is blocked by", "outward": "blocks"}, - "outwardIssue": map[string]interface{}{ + "outwardIssue": map[string]any{ "key": "PROJ-456", "fields": map[string]string{"summary": "Blocked issue"}, }, @@ -43,39 +42,39 @@ func TestRunList(t *testing.T) { defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runList(opts, "PROJ-123") - require.NoError(t, err) - assert.Contains(t, stdout.String(), "PROJ-456") - assert.Contains(t, stdout.String(), "Blocks") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "PROJ-456") + testutil.Contains(t, stdout.String(), "Blocks") // OutwardIssue is set → current issue is the inward side → show inward direction - assert.Contains(t, stdout.String(), "is blocked by") + testutil.Contains(t, stdout.String(), "is blocked by") } func TestRunList_NoLinks(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - json.NewEncoder(w).Encode(map[string]interface{}{ - "fields": map[string]interface{}{ - "issuelinks": []interface{}{}, + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{ + "fields": map[string]any{ + "issuelinks": []any{}, }, }) })) defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout, stderr bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &stderr} opts.SetAPIClient(client) err = runList(opts, "PROJ-123") - require.NoError(t, err) + testutil.RequireNoError(t, err) } func TestRunCreate(t *testing.T) { @@ -83,7 +82,7 @@ func TestRunCreate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/rest/api/3/issueLinkType": - json.NewEncoder(w).Encode(map[string]interface{}{ + _ = json.NewEncoder(w).Encode(map[string]any{ "issueLinkTypes": []map[string]string{ {"id": "1", "name": "Blocks", "inward": "is blocked by", "outward": "blocks"}, }, @@ -98,27 +97,27 @@ func TestRunCreate(t *testing.T) { defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runCreate(opts, "PROJ-123", "PROJ-456", "Blocks") - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Created") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "Created") var req api.CreateIssueLinkRequest err = json.Unmarshal(capturedBody, &req) - require.NoError(t, err) - assert.Equal(t, "Blocks", req.Type.Name) - assert.Equal(t, "PROJ-123", req.OutwardIssue.Key) - assert.Equal(t, "PROJ-456", req.InwardIssue.Key) + testutil.RequireNoError(t, err) + testutil.Equal(t, req.Type.Name, "Blocks") + testutil.Equal(t, req.OutwardIssue.Key, "PROJ-123") + testutil.Equal(t, req.InwardIssue.Key, "PROJ-456") } func TestRunCreate_InvalidType(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - json.NewEncoder(w).Encode(map[string]interface{}{ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{ "issueLinkTypes": []map[string]string{ {"id": "1", "name": "Blocks"}, {"id": "2", "name": "Relates"}, @@ -128,39 +127,39 @@ func TestRunCreate_InvalidType(t *testing.T) { defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) opts := &root.Options{Output: "table", Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runCreate(opts, "A", "B", "InvalidType") - require.Error(t, err) - assert.Contains(t, err.Error(), "not found") - assert.Contains(t, err.Error(), "Blocks") + testutil.RequireError(t, err) + testutil.Contains(t, err.Error(), "not found") + testutil.Contains(t, err.Error(), "Blocks") } func TestRunDelete(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/rest/api/3/issueLink/10001", r.URL.Path) + testutil.Equal(t, r.URL.Path, "/rest/api/3/issueLink/10001") w.WriteHeader(http.StatusNoContent) })) defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runDelete(opts, "10001") - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Deleted") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "Deleted") } func TestRunTypes(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - json.NewEncoder(w).Encode(map[string]interface{}{ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{ "issueLinkTypes": []map[string]string{ {"id": "1", "name": "Blocks", "inward": "is blocked by", "outward": "blocks"}, {"id": "2", "name": "Relates", "inward": "relates to", "outward": "relates to"}, @@ -170,14 +169,14 @@ func TestRunTypes(t *testing.T) { defer server.Close() client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@t.com", APIToken: "tok"}) - require.NoError(t, err) + testutil.RequireNoError(t, err) var stdout bytes.Buffer opts := &root.Options{Output: "table", Stdout: &stdout, Stderr: &bytes.Buffer{}} opts.SetAPIClient(client) err = runTypes(opts) - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Blocks") - assert.Contains(t, stdout.String(), "Relates") + testutil.RequireNoError(t, err) + testutil.Contains(t, stdout.String(), "Blocks") + testutil.Contains(t, stdout.String(), "Relates") } diff --git a/tools/jtk/internal/text/escapes.go b/tools/jtk/internal/text/escapes.go index 5be9021..50f83df 100644 --- a/tools/jtk/internal/text/escapes.go +++ b/tools/jtk/internal/text/escapes.go @@ -1,3 +1,4 @@ +// Package text provides text manipulation utilities. package text import "strings"