package credentials import ( "context" "errors" "os" "os/exec" "path/filepath" "strings" "testing" "git.ipao.vip/rogee/creator-hub/internal/account" ) const testCredentialValue = "sessionid=restart-secret; token=second" var testCredentialReference = account.CredentialReference{ID: "account-a-cookies", Provider: "os_keyring"} func TestPersistentCredentialBridgeStoreFailureLeavesNoFile(t *testing.T) { for _, failure := range []string{"chmod", "directory sync"} { t.Run(failure, func(t *testing.T) { directory := t.TempDir() bridge, err := NewPersistentCredentialBridge(directory, []byte("0123456789abcdef0123456789abcdef")) if err != nil { t.Fatal(err) } var chmodFile *os.File switch failure { case "chmod": bridge.chmod = func(file *os.File, _ os.FileMode) error { chmodFile = file return errors.New("chmod unavailable") } case "directory sync": calls := 0 bridge.syncDirectory = func(path string) error { calls++ if calls == 1 { return errors.New("directory sync unavailable") } return syncCredentialDirectory(path) } } if err := bridge.Store(context.Background(), testCredentialReference, "creatorhub/account-a/cookies", testCredentialValue); err == nil { t.Fatal("credential store failure returned success") } if chmodFile != nil { if _, err := chmodFile.Write([]byte("still-open")); err == nil { t.Fatal("credential temporary file remained open after chmod failure") } } entries, err := os.ReadDir(directory) if err != nil || len(entries) != 0 { t.Fatalf("credential store failure left files behind: entries=%v err=%v", entries, err) } }) } } func TestPersistentCredentialBridgeResolvesAndRejectsCorruption(t *testing.T) { bridge, err := NewPersistentCredentialBridge(t.TempDir(), []byte("0123456789abcdef0123456789abcdef")) if err != nil { t.Fatal(err) } key := "creatorhub/account-a/cookies" if err := bridge.Store(context.Background(), testCredentialReference, key, testCredentialValue); err != nil { t.Fatal(err) } value, err := bridge.Resolve(context.Background(), testCredentialReference, key) if err != nil || string(value) != testCredentialValue { t.Fatalf("resolve credential: %q %v", value, err) } path := bridge.path(testCredentialReference.Provider, key) if err := os.WriteFile(path, []byte("corrupted"), 0o600); err != nil { t.Fatal(err) } if _, err := bridge.Resolve(context.Background(), testCredentialReference, key); err == nil { t.Fatal("corrupted credential resolved") } if err := bridge.Delete(context.Background(), testCredentialReference, key); err != nil { t.Fatal(err) } if err := bridge.Delete(context.Background(), testCredentialReference, key); err != nil { t.Fatal(err) } } func TestPersistentCredentialBridgeStoresEncryptedCredentialAcrossProcesses(t *testing.T) { if action := os.Getenv("CREATORHUB_CREDENTIAL_HELPER_ACTION"); action != "" { bridge, err := NewPersistentCredentialBridge(os.Getenv("CREATORHUB_CREDENTIAL_HELPER_DIR"), []byte("0123456789abcdef0123456789abcdef")) if err != nil { t.Fatal(err) } switch action { case "store": err = bridge.Store(context.Background(), testCredentialReference, "creatorhub/account-a/cookies", testCredentialValue) case "delete": err = bridge.Delete(context.Background(), testCredentialReference, "creatorhub/account-a/cookies") } if err != nil { t.Fatal(err) } return } directory := t.TempDir() run := func(action string) { t.Helper() process := exec.Command(os.Args[0], "-test.run=^TestPersistentCredentialBridgeStoresEncryptedCredentialAcrossProcesses$") process.Env = append(os.Environ(), "CREATORHUB_CREDENTIAL_HELPER_ACTION="+action, "CREATORHUB_CREDENTIAL_HELPER_DIR="+directory) if output, err := process.CombinedOutput(); err != nil { t.Fatalf("credential helper %s: %v %s", action, err, output) } } run("store") files, err := filepath.Glob(filepath.Join(directory, "*.credential")) if err != nil || len(files) != 1 { t.Fatalf("persistent credential file missing: files=%v err=%v", files, err) } payload, err := os.ReadFile(files[0]) if err != nil || strings.Contains(string(payload), "restart-secret") { t.Fatalf("credential file is unreadable or contains plaintext: err=%v", err) } run("delete") if _, err := os.Stat(files[0]); !os.IsNotExist(err) { t.Fatalf("credential file remained after delete: %v", err) } }