diff --git a/README.md b/README.md index 870115c..a746da5 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ cat statement.csv | any2anexoj-cli --platform=trading212 - `table` (default): pretty terminal table - `csv`: the Anexo J CSV schema -- `html`: a complete, standalone Bootstrap-styled HTML report — a `` document with the Anexo J table embedded and Bootstrap loaded via CDN +- `html`: a complete, standalone Bootstrap-styled HTML report — a `` document with the Anexo J table embedded and the official Bootstrap 5.3.3 stylesheet inlined via `go:embed`, so the report renders fully offline The `table` and `html` formats are translated according to `--language`/`-l` (`pt` by default, `en` available). Other flags: `--selectors` filters which entries are processed (`code`, `assetCountry`), `--open-figi-api-key` raises OpenFIGI rate limits for faster report generation, and `--debug` enables debug logging. diff --git a/cmd/any2anexoj-cli/main_test.go b/cmd/any2anexoj-cli/main_test.go deleted file mode 100644 index d71cb85..0000000 --- a/cmd/any2anexoj-cli/main_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package main - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/spf13/pflag" -) - -// resetFlags puts every pflag-backed variable back to its default so each -// test that calls runWithIO sees a clean slate regardless of test ordering -// or arguments the previous test set via pflag.Set. -func resetFlags(t *testing.T) { - t.Helper() - if err := pflag.Set("platform", "trading212"); err != nil { - t.Fatalf("reset platform flag: %v", err) - } - if err := pflag.Set("language", "en"); err != nil { - t.Fatalf("reset language flag: %v", err) - } - if err := pflag.Set("debug", "false"); err != nil { - t.Fatalf("reset debug flag: %v", err) - } - if err := pflag.Set("format", "table"); err != nil { - t.Fatalf("reset format flag: %v", err) - } - if err := pflag.Set("open-figi-api-key", ""); err != nil { - t.Fatalf("reset open-figi-api-key flag: %v", err) - } - if err := pflag.Set("selectors", ""); err != nil { - t.Fatalf("reset selectors flag: %v", err) - } - if err := pflag.Set("state-file", ""); err != nil { - t.Fatalf("reset state-file flag: %v", err) - } -} - -// trading212SampleCSV is a minimal Trading212 export with a header row and -// one market buy + one matching market sell so BuildReport reaches EOF and -// exercises the store.Save path. The line format mirrors the fixtures in -// internal/trading212/record_test.go (20 columns). -const trading212SampleCSV = `Action,Time,ISIN,Ticker,Name,Notes,Quantity,Price,Price currency,Exchange rate,Result,Result currency,Charges,Charges currency,Stamp duty,Stamp duty currency,Conversion fee,Conversion fee currency,French transaction tax,French transaction tax currency -Market buy,2025-07-03 10:44:29,XX1234567890,ABXY,"Asparagus Broccoli",EOF987654321,2.4387014200,7.3690000000,USD,1.17995999,,"EUR",15.25,"EUR",0.25,"EUR",0.02,"EUR",, -Market sell,2025-08-04 11:45:30,XX1234567890,ABXY,"Asparagus Broccoli",EOF987654321,2.4387014200,7.9999999999,USD,1.17995999,,"EUR",15.25,"EUR",,,0.02,"EUR",0.1,"EUR" -` - -// runWithStdin runs runWithIO against the supplied stdin payload. -func runWithStdin(t *testing.T, stdin string, stdout *bytes.Buffer) error { - t.Helper() - if err := runWithIO(t.Context(), strings.NewReader(stdin), stdout); err != nil { - return err - } - return nil -} - -// TestRunWithIO_StateFileCreated verifies that running the CLI with -// --state-file produces a state file on disk after a successful EOF. -func TestRunWithIO_StateFileCreated(t *testing.T) { - resetFlags(t) - t.Cleanup(func() { resetFlags(t) }) - - dir := t.TempDir() - statePath := filepath.Join(dir, "state.json") - - if err := pflag.Set("state-file", statePath); err != nil { - t.Fatalf("set state-file flag: %v", err) - } - if err := pflag.Set("format", "csv"); err != nil { - t.Fatalf("set format flag: %v", err) - } - - var stdout bytes.Buffer - if err := runWithStdin(t, trading212SampleCSV, &stdout); err != nil { - t.Fatalf("runWithIO returned an error: %v\nstdout: %s", err, stdout.String()) - } - - info, err := os.Stat(statePath) - if err != nil { - t.Fatalf("expected state file at %s but stat returned error: %v", statePath, err) - } - if info.Size() == 0 { - t.Fatalf("state file at %s is empty", statePath) - } - - // State file must look like JSON with the expected version field. - body, err := os.ReadFile(statePath) - if err != nil { - t.Fatalf("read state file: %v", err) - } - if !bytes.Contains(body, []byte(`"version"`)) { - t.Errorf("state file missing version field, got: %s", body) - } - if !bytes.Contains(body, []byte(`"trading212"`)) { - t.Errorf("state file missing trading212 platform, got: %s", body) - } -} - -// TestRunWithIO_NoStateFileByDefault verifies that omitting --state-file -// behaves exactly like the pre-persistence CLI: nothing is written to disk -// and the report still renders. -func TestRunWithIO_NoStateFileByDefault(t *testing.T) { - resetFlags(t) - t.Cleanup(func() { resetFlags(t) }) - - // Use a temp working directory so any accidental file write would - // show up clearly via t.TempDir's cleanup listing. - dir := t.TempDir() - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("getwd: %v", err) - } - if err := os.Chdir(dir); err != nil { - t.Fatalf("chdir: %v", err) - } - t.Cleanup(func() { _ = os.Chdir(cwd) }) - - if err := pflag.Set("format", "csv"); err != nil { - t.Fatalf("set format flag: %v", err) - } - - var stdout bytes.Buffer - if err := runWithStdin(t, trading212SampleCSV, &stdout); err != nil { - t.Fatalf("runWithIO returned an error: %v\nstdout: %s", err, stdout.String()) - } - - entries, err := os.ReadDir(dir) - if err != nil { - t.Fatalf("read tmp dir: %v", err) - } - for _, e := range entries { - t.Errorf("unexpected file written without --state-file: %s", e.Name()) - } - - if stdout.Len() == 0 { - t.Errorf("expected non-empty csv output on stdout") - } -} - -// TestRunWithIO_HTMLFormat verifies that --format html produces a complete -// HTML document on stdout starting with the DOCTYPE declaration. -func TestRunWithIO_HTMLFormat(t *testing.T) { - resetFlags(t) - t.Cleanup(func() { resetFlags(t) }) - - if err := pflag.Set("format", "html"); err != nil { - t.Fatalf("set format flag: %v", err) - } - - var stdout bytes.Buffer - if err := runWithStdin(t, trading212SampleCSV, &stdout); err != nil { - t.Fatalf("runWithIO returned an error: %v\nstdout: %s", err, stdout.String()) - } - - got := stdout.String() - if !strings.HasPrefix(got, "") { - t.Errorf("expected HTML output to start with ; got prefix: %q", - got[:min(len(got), 64)]) - } - if !strings.Contains(got, "