package util import ( "errors" "fmt" "os" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/i18n" ) // Localisation slice 2 release B (R-557), scenarios S3 and its wrong case. // // A message-carrying error has to do three things at once, and each one is a different failure if it // is missing: print the Hungarian it always printed (parity), still satisfy the decision a caller // makes on it (R-553), and render in the reader's language at the display end (the point). // a real key from the bundle, with one parameter, so nothing here is a fixture of itself const oneParamKey = "flash.offbox.config_invalid" var errTestKind = errors.New("test-kind") func TestMsgErrorKeepsKindAndHuText(t *testing.T) { b, err := i18n.Load() if err != nil { t.Fatal(err) } e := MsgErrorf(errTestKind, oneParamKey, "a port nem szam") // 1. parity: Error() is the Hungarian sentence, byte for byte what fmt.Errorf produced. want := fmt.Sprintf(b.Msg(i18n.Default, oneParamKey), "a port nem szam") if e.Error() != want { t.Errorf("Error() is not the Hungarian sentence\n got %q\n want %q", e.Error(), want) } // And an un-converted printer — a log line, a %v — sees exactly that. if got := fmt.Sprintf("%v", e); got != want { t.Errorf("%%v printed %q, want %q", got, want) } // 2. the decision still reads: R-553's whole point. if !errors.Is(e, errTestKind) { t.Error("errors.Is no longer finds the kind — every status-code decision built on R-553 breaks") } if errors.Is(e, os.ErrNotExist) { t.Error("errors.Is matched an unrelated sentinel") } // 3. the display end renders in the reader's language. if en := ErrText("en", e); en == want || en == "" { t.Errorf("English render did not happen: %q", en) } if hu := ErrText(i18n.Default, e); hu != want { t.Errorf("Hungarian render moved:\n got %q\n want %q", hu, want) } } // TestMsgErrorUnwrapsTheCauseToo — the predecessor (KindErrorf) returned the kind ALONE from Unwrap, // so a wrapped cause was unreachable. A converted producer often wraps one (`%w` on an os or docker // error), and a caller that tested for it would have silently stopped matching. func TestMsgErrorUnwrapsTheCauseToo(t *testing.T) { cause := fmt.Errorf("open /x: %w", os.ErrNotExist) e := MsgErrorf(errTestKind, oneParamKey, cause) if !errors.Is(e, errTestKind) { t.Error("the kind is unreachable") } if !errors.Is(e, os.ErrNotExist) { t.Error("the wrapped CAUSE is unreachable — this is what KindErrorf lost") } } func TestErrTextFallsBackVerbatim(t *testing.T) { // The wrong case in S3: an error from restic, docker, ssh or the stdlib is not ours to translate. for _, e := range []error{ errors.New("repository is already locked by PID 1234"), fmt.Errorf("Error response from daemon: No such container: x"), os.ErrPermission, } { for _, lang := range []string{"hu", "en"} { if got := ErrText(lang, e); got != e.Error() { t.Errorf("%s: a foreign error was rewritten\n got %q\n want %q", lang, got, e.Error()) } } } if got := ErrText("en", nil); got != "" { t.Errorf("a nil error must render empty, got %q", got) } } // TestErrTextRendersAWrappedMessageErrorToo — a sentence wrapping a sentence. Both halves are ours, // so BOTH follow the language; that only works because the inner error is passed as an ARGUMENT // rather than as pre-rendered text. func TestErrTextRendersAWrappedMessageErrorToo(t *testing.T) { inner := MsgError("flash.offbox.app_missing") outer := MsgError(oneParamKey, inner) hu, en := ErrText("hu", outer), ErrText("en", outer) if hu == en { t.Fatalf("the wrapped chain did not follow the language at all: %q", hu) } b, _ := i18n.Load() if innerHU := b.Msg("hu", "flash.offbox.app_missing"); !contains(hu, innerHU) { t.Errorf("the Hungarian render lost the inner sentence\n got %q\n want it to contain %q", hu, innerHU) } if innerEN := b.Msg("en", "flash.offbox.app_missing"); !contains(en, innerEN) { t.Errorf("the English render kept the Hungarian inner sentence\n got %q\n want it to contain %q", en, innerEN) } } // TestAsMsgSeesThroughAFmtWrapper — a converted error that something else wrapped with `%w` still // answers AsMsg, so a printer further out does not lose the key. func TestAsMsgSeesThroughAFmtWrapper(t *testing.T) { e := fmt.Errorf("context: %w", MsgError("flash.offbox.app_missing")) m, ok := AsMsg(e) if !ok { t.Fatal("AsMsg lost the key through a fmt wrapper") } if m.Key() != "flash.offbox.app_missing" { t.Errorf("wrong key: %q", m.Key()) } // The OUTER text is fmt's, and it is what a caller printing the whole chain sees — errText // renders the innermost message it can find, which is the sentence a customer needs. Stated so // nobody reads this as the outer text being dropped by accident: a `%w` wrapper that adds // Hungarian of its own should itself be a MsgError, and release B converts those. if ErrText("en", e) == e.Error() { t.Error("the English render was identical to the Hungarian chain") } } func contains(s, sub string) bool { return len(sub) > 0 && len(s) >= len(sub) && (func() bool { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return true } } return false })() }