| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- package git
- import (
- "os"
- "path/filepath"
- "strings"
- "testing"
- )
- func newTestCredentialStore(t *testing.T) (*CredentialStore, *fakeDatabase) {
- t.Helper()
- database := newFakeDatabase()
- store, err := newCredentialStore(database, filepath.Join(t.TempDir(), "keys"))
- if err != nil {
- t.Fatalf("newCredentialStore() returned error: %v", err)
- }
- return store, database
- }
- func TestCredentialSaveAndGet(t *testing.T) {
- store, _ := newTestCredentialStore(t)
- if err := store.Save("toby", "github.com", "tobychui", "ghp_secret_token"); err != nil {
- t.Fatalf("Save() returned error: %v", err)
- }
- credential, found := store.Get("toby", "github.com")
- if !found {
- t.Fatalf("Get() = not found, want the saved credential")
- }
- if credential.Username != "tobychui" {
- t.Errorf("Username = %q, want %q", credential.Username, "tobychui")
- }
- if credential.Token != "ghp_secret_token" {
- t.Errorf("Token = %q, want %q", credential.Token, "ghp_secret_token")
- }
- if credential.Host != "github.com" {
- t.Errorf("Host = %q, want %q", credential.Host, "github.com")
- }
- }
- func TestCredentialTokenIsEncryptedAtRest(t *testing.T) {
- store, database := newTestCredentialStore(t)
- if err := store.Save("toby", "github.com", "tobychui", "ghp_super_secret"); err != nil {
- t.Fatalf("Save() returned error: %v", err)
- }
- entries, err := database.ListTable(credentialTable)
- if err != nil {
- t.Fatalf("ListTable() returned error: %v", err)
- }
- if len(entries) != 1 {
- t.Fatalf("stored %d records, want 1", len(entries))
- }
- raw := string(entries[0][1])
- if strings.Contains(raw, "ghp_super_secret") {
- t.Errorf("stored record contains the plaintext token: %s", raw)
- }
- if !strings.Contains(raw, "tobychui") {
- t.Errorf("stored record should keep the user name readable for pre-fill: %s", raw)
- }
- }
- func TestCredentialTokenIsNeverMarshalled(t *testing.T) {
- store, _ := newTestCredentialStore(t)
- if err := store.Save("toby", "github.com", "tobychui", "ghp_secret"); err != nil {
- t.Fatalf("Save() returned error: %v", err)
- }
- //List is what the front-end receives; it must not carry any secret
- for _, credential := range store.List("toby") {
- if credential.Token != "" {
- t.Errorf("List() returned a populated Token field, want it empty")
- }
- }
- }
- func TestCredentialsAreIsolatedPerUser(t *testing.T) {
- store, _ := newTestCredentialStore(t)
- if err := store.Save("alice", "github.com", "alice-gh", "alice-token"); err != nil {
- t.Fatalf("Save() returned error: %v", err)
- }
- if err := store.Save("bob", "github.com", "bob-gh", "bob-token"); err != nil {
- t.Fatalf("Save() returned error: %v", err)
- }
- alice, found := store.Get("alice", "github.com")
- if !found || alice.Token != "alice-token" {
- t.Errorf("Get(alice) = %+v, want alice's own token", alice)
- }
- bob, found := store.Get("bob", "github.com")
- if !found || bob.Token != "bob-token" {
- t.Errorf("Get(bob) = %+v, want bob's own token", bob)
- }
- if list := store.List("alice"); len(list) != 1 || list[0].Username != "alice-gh" {
- t.Errorf("List(alice) = %+v, want only alice's entry", list)
- }
- }
- func TestCredentialRemove(t *testing.T) {
- store, _ := newTestCredentialStore(t)
- if err := store.Save("toby", "github.com", "tobychui", "token"); err != nil {
- t.Fatalf("Save() returned error: %v", err)
- }
- if err := store.Remove("toby", "github.com"); err != nil {
- t.Fatalf("Remove() returned error: %v", err)
- }
- if _, found := store.Get("toby", "github.com"); found {
- t.Errorf("Get() found a credential after Remove(), want it gone")
- }
- //Removing something that is not there is not an error
- if err := store.Remove("toby", "github.com"); err != nil {
- t.Errorf("Remove() on a missing credential = %v, want nil", err)
- }
- }
- func TestCredentialSaveValidation(t *testing.T) {
- store, _ := newTestCredentialStore(t)
- tests := []struct {
- name string
- owner string
- host string
- remoteUser string
- token string
- }{
- {name: "empty owner", owner: "", host: "github.com", remoteUser: "u", token: "t"},
- {name: "empty host", owner: "toby", host: "", remoteUser: "u", token: "t"},
- {name: "empty token", owner: "toby", host: "github.com", remoteUser: "u", token: ""},
- {name: "whitespace token", owner: "toby", host: "github.com", remoteUser: "u", token: " "},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- if err := store.Save(test.owner, test.host, test.remoteUser, test.token); err == nil {
- t.Errorf("Save() with %s = nil error, want an error", test.name)
- }
- })
- }
- }
- func TestCredentialGetMissing(t *testing.T) {
- store, _ := newTestCredentialStore(t)
- tests := []struct {
- name string
- owner string
- host string
- }{
- {name: "nothing stored", owner: "toby", host: "github.com"},
- {name: "empty owner", owner: "", host: "github.com"},
- {name: "empty host", owner: "toby", host: ""},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- if _, found := store.Get(test.owner, test.host); found {
- t.Errorf("Get(%q, %q) = found, want not found", test.owner, test.host)
- }
- })
- }
- }
- func TestCredentialResolveForRemote(t *testing.T) {
- store, _ := newTestCredentialStore(t)
- if err := store.Save("toby", "github.com", "tobychui", "token"); err != nil {
- t.Fatalf("Save() returned error: %v", err)
- }
- tests := []struct {
- name string
- remoteURL string
- wantFound bool
- }{
- {name: "https url", remoteURL: "https://github.com/tobychui/arozos.git", wantFound: true},
- {name: "https url with credentials", remoteURL: "https://user@github.com/a/b.git", wantFound: true},
- {name: "scp style", remoteURL: "git@github.com:tobychui/arozos.git", wantFound: true},
- {name: "different host", remoteURL: "https://gitlab.com/a/b.git", wantFound: false},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- _, found := store.ResolveForRemote("toby", test.remoteURL)
- if found != test.wantFound {
- t.Errorf("ResolveForRemote(%q) found = %v, want %v", test.remoteURL, found, test.wantFound)
- }
- })
- }
- }
- func TestNormaliseHost(t *testing.T) {
- tests := []struct {
- name string
- input string
- want string
- }{
- {name: "bare host", input: "github.com", want: "github.com"},
- {name: "mixed case", input: "GitHub.COM", want: "github.com"},
- {name: "surrounding spaces", input: " github.com ", want: "github.com"},
- {name: "https url", input: "https://github.com/a/b.git", want: "github.com"},
- {name: "http url with port", input: "http://git.local:3000/a/b.git", want: "git.local"},
- {name: "scp syntax", input: "git@github.com:a/b.git", want: "github.com"},
- {name: "host with port", input: "git.local:3000", want: "git.local"},
- {name: "empty", input: "", want: ""},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- if got := normaliseHost(test.input); got != test.want {
- t.Errorf("normaliseHost(%q) = %q, want %q", test.input, got, test.want)
- }
- })
- }
- }
- func TestEncryptDecryptRoundTrip(t *testing.T) {
- key := make([]byte, 32)
- for i := range key {
- key[i] = byte(i)
- }
- tests := []struct {
- name string
- plaintext string
- }{
- {name: "simple token", plaintext: "ghp_abcdefghijklmnop"},
- {name: "empty string", plaintext: ""},
- {name: "unicode", plaintext: "pässwörd-中文"},
- {name: "long secret", plaintext: strings.Repeat("s", 4096)},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- encrypted, err := encryptSecret(key, test.plaintext)
- if err != nil {
- t.Fatalf("encryptSecret() returned error: %v", err)
- }
- if encrypted == test.plaintext && test.plaintext != "" {
- t.Fatalf("encryptSecret() returned the plaintext unchanged")
- }
- decrypted, err := decryptSecret(key, encrypted)
- if err != nil {
- t.Fatalf("decryptSecret() returned error: %v", err)
- }
- if decrypted != test.plaintext {
- t.Errorf("round trip = %q, want %q", decrypted, test.plaintext)
- }
- })
- }
- }
- func TestEncryptSecretUsesFreshNonce(t *testing.T) {
- key := make([]byte, 32)
- first, err := encryptSecret(key, "same input")
- if err != nil {
- t.Fatalf("encryptSecret() returned error: %v", err)
- }
- second, err := encryptSecret(key, "same input")
- if err != nil {
- t.Fatalf("encryptSecret() returned error: %v", err)
- }
- if first == second {
- t.Errorf("encrypting the same value twice produced identical ciphertext, want distinct nonces")
- }
- }
- func TestDecryptSecretRejectsTampering(t *testing.T) {
- key := make([]byte, 32)
- otherKey := make([]byte, 32)
- otherKey[0] = 0xFF
- encrypted, err := encryptSecret(key, "secret")
- if err != nil {
- t.Fatalf("encryptSecret() returned error: %v", err)
- }
- tests := []struct {
- name string
- key []byte
- input string
- }{
- {name: "wrong key", key: otherKey, input: encrypted},
- {name: "not base64", key: key, input: "!!!not base64!!!"},
- {name: "truncated", key: key, input: "AAAA"},
- {name: "empty", key: key, input: ""},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- if _, err := decryptSecret(test.key, test.input); err == nil {
- t.Errorf("decryptSecret() with %s = nil error, want an error", test.name)
- }
- })
- }
- }
- func TestLoadOrCreateKeyIsStable(t *testing.T) {
- keyStore := filepath.Join(t.TempDir(), "keys")
- first, err := loadOrCreateKey(keyStore)
- if err != nil {
- t.Fatalf("loadOrCreateKey() returned error: %v", err)
- }
- if len(first) != 32 {
- t.Fatalf("key length = %d, want 32", len(first))
- }
- second, err := loadOrCreateKey(keyStore)
- if err != nil {
- t.Fatalf("loadOrCreateKey() second call returned error: %v", err)
- }
- if string(first) != string(second) {
- t.Errorf("loadOrCreateKey() returned a different key on the second call")
- }
- }
- func TestLoadOrCreateKeyRegeneratesDamagedFile(t *testing.T) {
- keyStore := filepath.Join(t.TempDir(), "keys")
- if _, err := loadOrCreateKey(keyStore); err != nil {
- t.Fatalf("loadOrCreateKey() returned error: %v", err)
- }
- if err := os.WriteFile(filepath.Join(keyStore, keyFileName), []byte("corrupted"), 0600); err != nil {
- t.Fatalf("cannot damage key file: %v", err)
- }
- key, err := loadOrCreateKey(keyStore)
- if err != nil {
- t.Fatalf("loadOrCreateKey() on a damaged file returned error: %v", err)
- }
- if len(key) != 32 {
- t.Errorf("regenerated key length = %d, want 32", len(key))
- }
- }
- func TestNewCredentialStoreValidation(t *testing.T) {
- tests := []struct {
- name string
- database CredentialDatabase
- keyStorePath string
- }{
- {name: "nil database", database: nil, keyStorePath: t.TempDir()},
- {name: "empty key path", database: newFakeDatabase(), keyStorePath: ""},
- }
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- if _, err := newCredentialStore(test.database, test.keyStorePath); err == nil {
- t.Errorf("newCredentialStore() with %s = nil error, want an error", test.name)
- }
- })
- }
- }
- func TestManagerWithoutDatabaseHasNoCredentialStore(t *testing.T) {
- manager, err := NewManager(Options{})
- if err != nil {
- t.Fatalf("NewManager() returned error: %v", err)
- }
- if manager.Credentials() != nil {
- t.Errorf("Credentials() = non-nil without a database, want nil")
- }
- }
|