package fikenconfig import ( "encoding/json" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "simenandre.no/fiken/fiken" "simenandre.no/fiken/fikenauth" ) // fakeFiken answers /user and /companies per bearer token, so each token is a // different Fiken user. type fakeFiken struct { users map[string]fiken.User companies map[string][]fiken.Company asked int } func (f *fakeFiken) start(t *testing.T) *httptest.Server { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") user, ok := f.users[token] if !ok { w.WriteHeader(http.StatusUnauthorized) return } switch r.URL.Path { case "/user": json.NewEncoder(w).Encode(user) case "/companies": f.asked++ w.Header().Set("Fiken-Api-Page-Count", "1") json.NewEncoder(w).Encode(f.companies[token]) case "/oauth/revoke": w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusNotFound) } })) t.Cleanup(server.Close) return server } func open(t *testing.T, server *httptest.Server, dir string) *Config { t.Helper() c, err := Open(dir) if err != nil { t.Fatalf("Open: %v", err) } c.LoginCommand = "tool login" c.ClientOptions = []fiken.Option{fiken.WithBaseURL(server.URL)} c.AuthOverride = func(a *fikenauth.Config) { a.RevokeURL = server.URL + "/oauth/revoke" a.TokenURL = server.URL + "/oauth/token" } return c } func twoUsers() *fakeFiken { return &fakeFiken{ users: map[string]fiken.User{ "simen-token": {Name: "Simen", Email: "Simen@Sokkel.io"}, "bjerk-token": {Name: "Bjerk", Email: "post@bjerk.io"}, }, companies: map[string][]fiken.Company{ "simen-token": {{Name: "Sokkel AS", Slug: "sokkel-as"}}, "bjerk-token": {{Name: "Bjerk AS", Slug: "bjerk"}}, }, } } func TestEachCompanyGetsTheAccountThatReachesIt(t *testing.T) { t.Setenv(ClientIDEnv, "") t.Setenv(ClientSecretEnv, "") fake := twoUsers() server := fake.start(t) dir := t.TempDir() c := open(t, server, dir) if _, err := c.AddToken(t.Context(), "bjerk-token"); err != nil { t.Fatalf("AddToken: %v", err) } email, err := c.AddGrant(t.Context(), fikenauth.Token{ AccessToken: "simen-token", RefreshToken: "r", ExpiresAt: time.Now().Add(time.Hour), }) if err != nil { t.Fatalf("AddGrant: %v", err) } if email != "simen@sokkel.io" { t.Errorf("email = %q, want it lowercased", email) } // Another program, opening the same directory afterwards. other := open(t, server, dir) for company, want := range map[string]string{"sokkel-as": "simen@sokkel.io", "bjerk": "post@bjerk.io"} { if got, _ := other.AccountFor(company); got != want { t.Errorf("AccountFor(%s) = %q, want %q", company, got, want) } client, err := other.Client(t.Context(), company) if err != nil { t.Fatalf("Client(%s): %v", company, err) } if _, err := client.Companies(t.Context()); err != nil { t.Errorf("Client(%s) could not reach Fiken: %v", company, err) } } for _, path := range []string{other.Path(), filepath.Join(dir, "accounts", "simen@sokkel.io.json")} { info, err := os.Stat(path) if err != nil { t.Fatal(err) } if info.Mode().Perm() != 0o600 { t.Errorf("%s is %v, want 0600", path, info.Mode().Perm()) } } } func TestACompanyAddedSinceIsFoundByAskingAgain(t *testing.T) { fake := twoUsers() server := fake.start(t) c := open(t, server, t.TempDir()) if _, err := c.AddToken(t.Context(), "simen-token"); err != nil { t.Fatal(err) } fake.companies["simen-token"] = append(fake.companies["simen-token"], fiken.Company{Name: "Ny AS", Slug: "ny-as"}) if _, err := c.Client(t.Context(), "ny-as"); err != nil { t.Fatalf("Client(ny-as): %v", err) } reopened := open(t, server, c.dir) if _, ok := reopened.AccountFor("ny-as"); !ok { t.Error("the company found by asking again was not saved") } _, err := c.Client(t.Context(), "nobody-as") if !errors.Is(err, ErrNoAccount) { t.Fatalf("err = %v, want ErrNoAccount", err) } for _, want := range []string{"ny-as, sokkel-as", "`tool login`"} { if !strings.Contains(err.Error(), want) { t.Errorf("error %q does not mention %q", err, want) } } } func TestNobodyLoggedIn(t *testing.T) { c := open(t, twoUsers().start(t), t.TempDir()) _, err := c.Client(t.Context(), "sokkel-as") if !errors.Is(err, ErrNoAccount) || !strings.Contains(err.Error(), "nobody is logged in") { t.Errorf("err = %v", err) } } func TestTheEnvironmentOverridesTheApp(t *testing.T) { c := &Config{App: App{ClientID: "file-id", ClientSecret: "file-secret"}} t.Setenv(ClientIDEnv, "") t.Setenv(ClientSecretEnv, "env-secret") got := c.Auth() if got.ClientID != "file-id" || got.ClientSecret != "env-secret" { t.Errorf("Auth = %q / %q", got.ClientID, got.ClientSecret) } } func TestLogoutForgetsTheAccountAndItsGrant(t *testing.T) { server := twoUsers().start(t) c := open(t, server, t.TempDir()) email, err := c.AddGrant(t.Context(), fikenauth.Token{ AccessToken: "simen-token", RefreshToken: "r", ExpiresAt: time.Now().Add(time.Hour), }) if err != nil { t.Fatal(err) } if err := c.Logout(t.Context(), email); err != nil { t.Fatalf("Logout: %v", err) } if _, err := os.Stat(filepath.Join(c.dir, "accounts", email+".json")); !os.IsNotExist(err) { t.Errorf("the grant file is still there: %v", err) } if _, ok := open(t, server, c.dir).AccountFor("sokkel-as"); ok { t.Error("the account is still configured") } } func TestAnEmailCannotEscapeTheAccountsDirectory(t *testing.T) { c := &Config{dir: t.TempDir()} for _, email := range []string{"", "../x", "a/b", ".hidden"} { if _, err := c.Source(email); err == nil { t.Errorf("Source(%q) was accepted", email) } } } func TestDir(t *testing.T) { t.Setenv(DirEnv, "") t.Setenv("XDG_CONFIG_HOME", "/xdg") if got, _ := Dir(); got != "/xdg/fiken" { t.Errorf("Dir = %q", got) } t.Setenv(DirEnv, "/explicit") if got, _ := Dir(); got != "/explicit" { t.Errorf("Dir = %q", got) } }