diff options
| author | Simen A. W. Olsen <hello@simenandre.no> | 2026-11-03 22:08:49 +0100 |
|---|---|---|
| committer | Simen A. W. Olsen <hello@simenandre.no> | 2026-11-03 22:08:49 +0100 |
| commit | bef3ccf4521d4ed431f3cdfc66e618c619ce28e4 (patch) | |
| tree | ef8e6ff90fed4b27d4f2cfd082106f2efdb38ca5 /fikenconfig/config_test.go | |
| parent | a5491c9dccdf4678bf3b4380d208cf2562afeee4 (diff) | |
| download | fiken-43dc4f31b0d28b817cf74c2e91c5f31471214349.tar.gz fiken-43dc4f31b0d28b817cf74c2e91c5f31471214349.zip | |
fikenconfig keeps an OAuth app and a grant per account in ~/.config/fiken, and finds the account that reaches a company. Renewing a grant takes a lock, so programs sharing it take turns.
Diffstat (limited to 'fikenconfig/config_test.go')
| -rw-r--r-- | fikenconfig/config_test.go | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/fikenconfig/config_test.go b/fikenconfig/config_test.go new file mode 100644 index 0000000..2e89b7c --- /dev/null +++ b/fikenconfig/config_test.go @@ -0,0 +1,213 @@ +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) + } +} |
