Files
any2anexoj/cmd/any2anexoj-cli/main_test.go
T
natercioandnatercio 9bd4230ff1
Badges / coveralls (push) Successful in 1m4s
Internal state persistence (#27)
Allow us to generate year over year reports without having to rerun everything from the beginning.

Co-authored-by: Natercio Moniz <[email protected]>
2026-08-03 00:33:06 +01:00

169 lines
5.6 KiB
Go

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_UnsupportedPlatformForPersistence verifies that using
// --state-file with an unknown platform surfaces a clear error rather than
// silently falling back to EphemeralStore.
func TestRunWithIO_UnsupportedPlatformForPersistence(t *testing.T) {
resetFlags(t)
t.Cleanup(func() { resetFlags(t) })
if err := pflag.Set("state-file", filepath.Join(t.TempDir(), "state.json")); err != nil {
t.Fatalf("set state-file flag: %v", err)
}
// Currently only trading212 is wired through buildStore, but the
// reader switch also only supports trading212, so the reader error
// fires first. Either error is acceptable; we just need a clear
// failure message.
if err := pflag.Set("platform", "unknown-broker"); err != nil {
t.Fatalf("set platform flag: %v", err)
}
var stdout bytes.Buffer
err := runWithStdin(t, trading212SampleCSV, &stdout)
if err == nil {
t.Fatalf("expected an error for unsupported platform")
}
if !strings.Contains(err.Error(), "platform") {
t.Errorf("expected error to mention platform, got: %v", err)
}
}