aboutsummaryrefslogtreecommitdiffstats
path: root/fikenconfig/config_test.go
diff options
context:
space:
mode:
authorSimen A. W. Olsen <hello@simenandre.no>2026-11-03 22:08:49 +0100
committerSimen A. W. Olsen <hello@simenandre.no>2026-11-03 22:08:49 +0100
commitbef3ccf4521d4ed431f3cdfc66e618c619ce28e4 (patch)
treeef8e6ff90fed4b27d4f2cfd082106f2efdb38ca5 /fikenconfig/config_test.go
parenta5491c9dccdf4678bf3b4380d208cf2562afeee4 (diff)
downloadfiken-main.tar.gz
fiken-main.zip
Share one Fiken login between programsHEADv0.4.0main
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.go213
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)
+ }
+}