package auth import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "gitea.parodia.dev/drwily/petal/internal/db" ) // patchMe drives UpdateMeHandler as the given user would reach it: behind the // middleware, which is the only thing that puts an id in the context. func patchMe(t *testing.T, users *UserStore, id, body string) *httptest.ResponseRecorder { t.Helper() r := httptest.NewRequest(http.MethodPatch, "/me", strings.NewReader(body)) r = r.WithContext(WithUser(r.Context(), id)) w := httptest.NewRecorder() users.UpdateMeHandler()(w, r) return w } func TestSetPairLang(t *testing.T) { _, users, _ := newStores(t) if err := users.SetPairLang("bob", "pt-PT"); err != nil { t.Fatalf("set pt-PT: %v", err) } if u, _ := users.Get("bob"); u.PairLang != "pt-PT" { t.Fatalf("pair_lang = %q, want pt-PT", u.PairLang) } // Every pair with a langpack, not just the first one: this list and the // frontend's PACKS are two copies of the same fact, and the day they // disagree is the day she can pick a pair the app cannot render. if err := users.SetPairLang("bob", "fr"); err != nil { t.Fatalf("set fr: %v", err) } if u, _ := users.Get("bob"); u.PairLang != "fr" { t.Fatalf("pair_lang = %q, want fr", u.PairLang) } // And back — a writer who tries a pair and doesn't like it must be able to // return, which is the whole reason the picker exists. if err := users.SetPairLang("bob", "zh"); err != nil { t.Fatalf("set zh: %v", err) } if u, _ := users.Get("bob"); u.PairLang != "zh" { t.Fatalf("pair_lang = %q, want zh", u.PairLang) } } // A pair the frontend has no langpack for must not be storable. Accepting it // would leave her looking at Chinese copy with no way back except a lucky guess. func TestSetPairLangRejectsUnshippedPairs(t *testing.T) { _, users, _ := newStores(t) // "es" is the real case here — the pair whose pack has not been written yet. // "pt-BR" is the near-miss that matters most: a Brazilian code must not be // quietly served European copy and a European voice. for _, lang := range []string{"es", "pt-BR", "fr-CA", "klingon", "", " "} { if err := users.SetPairLang("bob", lang); err == nil { t.Fatalf("stored unshipped pair %q", lang) } } if u, _ := users.Get("bob"); u.PairLang != "zh" { t.Fatalf("a refused write still moved pair_lang to %q", u.PairLang) } } func TestSetPairLangUnknownUser(t *testing.T) { _, users, _ := newStores(t) if err := users.SetPairLang("nobody", "pt-PT"); err == nil { t.Fatal("set a pair language on an account that does not exist") } } func TestUpdateMeHandler(t *testing.T) { _, users, _ := newStores(t) w := patchMe(t, users, "bob", `{"pair_lang":"pt-PT"}`) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200 (%s)", w.Code, w.Body.String()) } // The whole user comes back, so the client can re-read the pair from the // server instead of assuming its request took. var got db.User if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode: %v", err) } if got.ID != "bob" || got.PairLang != "pt-PT" { t.Fatalf("response = %+v, want bob on pt-PT", got) } } func TestUpdateMeHandlerRejects(t *testing.T) { _, users, _ := newStores(t) for name, body := range map[string]string{ "unshipped pair": `{"pair_lang":"es"}`, "missing field": `{}`, "not json": `pt-PT`, } { if w := patchMe(t, users, "bob", body); w.Code != http.StatusBadRequest { t.Fatalf("%s: status = %d, want 400", name, w.Code) } } if u, _ := users.Get("bob"); u.PairLang != "zh" { t.Fatalf("a rejected request still moved pair_lang to %q", u.PairLang) } // A caller the middleware never resolved (or whose row is gone) is a lapsed // session, not a bad request — the client turns 401 into the sign-in overlay. if w := patchMe(t, users, "nobody", `{"pair_lang":"pt-PT"}`); w.Code != http.StatusUnauthorized { t.Fatalf("unknown user: status = %d, want 401", w.Code) } }