1874 lines
51 KiB
Go
1874 lines
51 KiB
Go
package appstate
|
|
|
|
import (
|
|
"errors"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.julianfamily.org/keepassgo/apiapproval"
|
|
"git.julianfamily.org/keepassgo/apitokens"
|
|
"git.julianfamily.org/keepassgo/session"
|
|
"git.julianfamily.org/keepassgo/vault"
|
|
"git.julianfamily.org/keepassgo/webdav"
|
|
)
|
|
|
|
func TestVisibleEntriesFollowsCurrentPathWithoutSearch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Crew", "Internet"}},
|
|
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Security Office"}},
|
|
},
|
|
},
|
|
},
|
|
CurrentPath: []string{"Crew", "Internet"},
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
|
|
titles := make([]string, 0, len(got))
|
|
for _, entry := range got {
|
|
titles = append(titles, entry.Title)
|
|
}
|
|
|
|
if !slices.Equal(titles, []string{"Bellagio", "Vault Console"}) {
|
|
t.Fatalf("visible titles = %v, want [Bellagio Vault Console]", titles)
|
|
}
|
|
}
|
|
|
|
func TestVisibleEntriesAtParentGroupOnlyShowsDirectEntries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "joe-note", Title: "Crew Note", Path: []string{"Crew"}},
|
|
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Crew", "Internet"}},
|
|
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Security Office"}},
|
|
},
|
|
},
|
|
},
|
|
CurrentPath: []string{"Crew"},
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
|
|
titles := make([]string, 0, len(got))
|
|
for _, entry := range got {
|
|
titles = append(titles, entry.Title)
|
|
}
|
|
if !slices.Equal(titles, []string{"Crew Note"}) {
|
|
t.Fatalf("visible titles = %v, want only direct entries from Crew", titles)
|
|
}
|
|
}
|
|
|
|
func TestPendingApprovalsReturnsManagerRequests(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Approvals: &stubApprovalManager{
|
|
pending: []apiapproval.Request{
|
|
{ID: "approval-1", TokenName: "CLI", Operation: apitokens.OperationListEntries},
|
|
},
|
|
},
|
|
}
|
|
|
|
got := state.PendingApprovals()
|
|
if len(got) != 1 || got[0].ID != "approval-1" {
|
|
t.Fatalf("PendingApprovals() = %#v, want approval-1", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveApprovalDelegatesToManager(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := &stubApprovalManager{}
|
|
state := State{Approvals: manager}
|
|
if err := state.ResolveApproval("approval-1", apiapproval.OutcomeAllowPermanent); err != nil {
|
|
t.Fatalf("ResolveApproval() error = %v", err)
|
|
}
|
|
if manager.lastID != "approval-1" || manager.lastOutcome != apiapproval.OutcomeAllowPermanent {
|
|
t.Fatalf("ResolveApproval() delegated (%q, %q), want (approval-1, allow-permanent)", manager.lastID, manager.lastOutcome)
|
|
}
|
|
}
|
|
|
|
func TestIssueRotateDisableRevokeAndDeleteAPIToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
session := &mutableStubSession{model: vault.Model{}}
|
|
state := State{Session: session}
|
|
now := time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC)
|
|
expiresAt := now.Add(24 * time.Hour)
|
|
|
|
issued, secret, err := state.IssueAPIToken("CLI", "grpc-cli", &expiresAt, now)
|
|
if err != nil {
|
|
t.Fatalf("IssueAPIToken() error = %v", err)
|
|
}
|
|
if issued.ID == "" || secret == "" {
|
|
t.Fatalf("IssueAPIToken() = %#v, %q, want non-empty id and secret", issued, secret)
|
|
}
|
|
|
|
tokens, err := state.APITokens()
|
|
if err != nil {
|
|
t.Fatalf("APITokens() error = %v", err)
|
|
}
|
|
if len(tokens) != 1 || tokens[0].ID != issued.ID {
|
|
t.Fatalf("APITokens() = %#v, want issued token", tokens)
|
|
}
|
|
|
|
rotated, rotatedSecret, err := state.RotateAPIToken(issued.ID, now.Add(time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("RotateAPIToken() error = %v", err)
|
|
}
|
|
if rotated.ID != issued.ID || rotatedSecret == "" || rotatedSecret == secret {
|
|
t.Fatalf("RotateAPIToken() = %#v, %q, want same id and new secret", rotated, rotatedSecret)
|
|
}
|
|
|
|
if err := state.DisableAPIToken(issued.ID); err != nil {
|
|
t.Fatalf("DisableAPIToken() error = %v", err)
|
|
}
|
|
if err := state.RevokeAPIToken(issued.ID, now.Add(2*time.Hour)); err != nil {
|
|
t.Fatalf("RevokeAPIToken() error = %v", err)
|
|
}
|
|
|
|
tokens, err = state.APITokens()
|
|
if err != nil {
|
|
t.Fatalf("APITokens() after revoke error = %v", err)
|
|
}
|
|
if len(tokens) != 1 || !tokens[0].Disabled || tokens[0].RevokedAt == nil {
|
|
t.Fatalf("APITokens() after revoke = %#v, want disabled revoked token", tokens)
|
|
}
|
|
|
|
if err := state.DeleteAPIToken(issued.ID); err != nil {
|
|
t.Fatalf("DeleteAPIToken() error = %v", err)
|
|
}
|
|
tokens, err = state.APITokens()
|
|
if err != nil {
|
|
t.Fatalf("APITokens() after delete error = %v", err)
|
|
}
|
|
if len(tokens) != 0 {
|
|
t.Fatalf("APITokens() after delete = %#v, want empty", tokens)
|
|
}
|
|
}
|
|
|
|
func TestRemoteProfilesReturnsVaultProfiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
RemoteProfiles: []vault.RemoteProfile{
|
|
{
|
|
ID: "bellagio-webdav",
|
|
Name: "Bellagio Vault",
|
|
Backend: vault.RemoteBackendWebDAV,
|
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
|
Path: "files/bellagio/keepass.kdbx",
|
|
},
|
|
{
|
|
ID: "archive-webdav",
|
|
Name: "Archive Vault",
|
|
Backend: vault.RemoteBackendWebDAV,
|
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
|
Path: "files/bellagio/archive.kdbx",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got, err := state.RemoteProfiles()
|
|
if err != nil {
|
|
t.Fatalf("RemoteProfiles() error = %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("len(RemoteProfiles()) = %d, want 2", len(got))
|
|
}
|
|
if got[0].ID != "archive-webdav" || got[1].ID != "bellagio-webdav" {
|
|
t.Fatalf("RemoteProfiles() = %#v, want sorted by name/id", got)
|
|
}
|
|
}
|
|
|
|
func TestRemoteCredentialEntriesReturnsSortedVaultEntries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "cred-2", Title: "Zulu Sign-In", Username: "zuser", Path: []string{"Crew", "Internet"}},
|
|
{ID: "cred-1", Title: "Alpha Sign-In", Username: "auser", Path: []string{"Crew", "Internet"}},
|
|
{ID: "cred-3", Title: "Mint Sign-In", Username: "frankcatton", Path: []string{"Crew", "Safe House"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got, err := state.RemoteCredentialEntries()
|
|
if err != nil {
|
|
t.Fatalf("RemoteCredentialEntries() error = %v", err)
|
|
}
|
|
if len(got) != 3 {
|
|
t.Fatalf("len(RemoteCredentialEntries()) = %d, want 3", len(got))
|
|
}
|
|
if got[0].ID != "cred-1" || got[1].ID != "cred-3" || got[2].ID != "cred-2" {
|
|
t.Fatalf("RemoteCredentialEntries() = %#v, want entries sorted by title", got)
|
|
}
|
|
}
|
|
|
|
func TestVisibleEntriesUsesGlobalSearchWhenQueryPresent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
|
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}},
|
|
{ID: "surveillance-console", Title: "Surveillance Console", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Security Office"}},
|
|
},
|
|
},
|
|
},
|
|
CurrentPath: []string{"Crew", "Internet"},
|
|
SearchQuery: "surveillance",
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
|
|
if len(got) != 1 || got[0].Title != "Surveillance Console" {
|
|
t.Fatalf("VisibleEntries() = %#v, want Security Office search match", got)
|
|
}
|
|
}
|
|
|
|
func TestVisibleEntriesReturnsDescendantsAfterClearingSearch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
|
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}},
|
|
{ID: "surveillance-console", Title: "Surveillance Console", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Security Office"}},
|
|
},
|
|
},
|
|
},
|
|
CurrentPath: []string{"Crew"},
|
|
SearchQuery: "missing",
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() with search error = %v", err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Fatalf("VisibleEntries() with missing search = %#v, want empty", got)
|
|
}
|
|
|
|
state.SearchQuery = ""
|
|
got, err = state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() after clearing search error = %v", err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Fatalf("len(VisibleEntries()) after clearing search = %d, want 0 direct entries at Crew", len(got))
|
|
}
|
|
}
|
|
|
|
func TestVisibleEntriesUsesTemplateSection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Templates: []vault.Entry{
|
|
{ID: "tpl-1", Title: "Website Login", Path: []string{"Templates", "Web"}},
|
|
{ID: "tpl-2", Title: "SSH Login", Path: []string{"Templates", "Infra"}},
|
|
},
|
|
},
|
|
},
|
|
Section: SectionTemplates,
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
|
|
if len(got) != 2 {
|
|
t.Fatalf("len(VisibleEntries()) = %d, want 2 templates", len(got))
|
|
}
|
|
}
|
|
|
|
func TestVisibleEntriesUsesRecycleBinSection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
RecycleBin: []vault.Entry{
|
|
{ID: "entry-1", Title: "Deleted Entry", Path: []string{"Root", "Internet"}},
|
|
},
|
|
},
|
|
},
|
|
Section: SectionRecycleBin,
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
|
|
if len(got) != 1 || got[0].ID != "entry-1" {
|
|
t.Fatalf("VisibleEntries() = %#v, want recycle-bin entry", got)
|
|
}
|
|
}
|
|
|
|
func TestVisibleEntriesUsesGlobalSearchWithinTemplateSection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Templates: []vault.Entry{
|
|
{ID: "tpl-1", Title: "Website Login", URL: "https://accounts.example.com", Path: []string{"Templates", "Web"}},
|
|
{ID: "tpl-2", Title: "SSH Login", URL: "ssh://infra.internal", Path: []string{"Templates", "Infra"}},
|
|
},
|
|
},
|
|
},
|
|
Section: SectionTemplates,
|
|
CurrentPath: []string{"Templates", "Web"},
|
|
SearchQuery: "infra",
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
|
|
if len(got) != 1 || got[0].ID != "tpl-2" {
|
|
t.Fatalf("VisibleEntries() = %#v, want global template search result tpl-2", got)
|
|
}
|
|
}
|
|
|
|
func TestVisibleEntriesResetToCurrentTemplatePathAfterClearingSearch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Templates: []vault.Entry{
|
|
{ID: "tpl-1", Title: "Website Login", Path: []string{"Templates", "Web"}},
|
|
{ID: "tpl-2", Title: "Email Login", Path: []string{"Templates", "Web"}},
|
|
{ID: "tpl-3", Title: "SSH Login", Path: []string{"Templates", "Infra"}},
|
|
},
|
|
},
|
|
},
|
|
Section: SectionTemplates,
|
|
CurrentPath: []string{"Templates", "Web"},
|
|
SearchQuery: "ssh",
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() with search error = %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].ID != "tpl-3" {
|
|
t.Fatalf("VisibleEntries() with search = %#v, want tpl-3", got)
|
|
}
|
|
|
|
state.SearchQuery = ""
|
|
got, err = state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() after clearing search error = %v", err)
|
|
}
|
|
|
|
if len(got) != 2 {
|
|
t.Fatalf("len(VisibleEntries()) after clearing search = %d, want 2", len(got))
|
|
}
|
|
if titles := []string{got[0].Title, got[1].Title}; !slices.Equal(titles, []string{"Email Login", "Website Login"}) {
|
|
t.Fatalf("VisibleEntries() after clearing search titles = %v, want [Email Login Website Login]", titles)
|
|
}
|
|
}
|
|
|
|
func TestVisibleEntriesUsesGlobalSearchWithinRecycleBin(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
RecycleBin: []vault.Entry{
|
|
{ID: "deleted-1", Title: "Deleted Bellagio", Path: []string{"Root", "Internet"}},
|
|
{ID: "deleted-2", Title: "Deleted Vault Vent", URL: "https://climate.example.com", Path: []string{"Root", "Safe House"}},
|
|
},
|
|
},
|
|
},
|
|
Section: SectionRecycleBin,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SearchQuery: "climate",
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
|
|
if len(got) != 1 || got[0].ID != "deleted-2" {
|
|
t.Fatalf("VisibleEntries() = %#v, want global recycle-bin search result deleted-2", got)
|
|
}
|
|
}
|
|
|
|
func TestSearchPathContextIncludesSectionRoots(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
section Section
|
|
entry vault.Entry
|
|
want string
|
|
}{
|
|
{
|
|
name: "entries use direct path",
|
|
section: SectionEntries,
|
|
entry: vault.Entry{Path: []string{"Root", "Internet"}},
|
|
want: "Root / Internet",
|
|
},
|
|
{
|
|
name: "templates retain templates root",
|
|
section: SectionTemplates,
|
|
entry: vault.Entry{Path: []string{"Templates", "Web"}},
|
|
want: "Templates / Web",
|
|
},
|
|
{
|
|
name: "recycle bin prefixes root label",
|
|
section: SectionRecycleBin,
|
|
entry: vault.Entry{Path: []string{"Root", "Internet"}},
|
|
want: "Recycle Bin / Root / Internet",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{Section: tt.section}
|
|
if got := state.SearchPathContext(tt.entry); got != tt.want {
|
|
t.Fatalf("SearchPathContext(%v) = %q, want %q", tt.entry.Path, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChildGroupsUsesCurrentModelAndCurrentPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
|
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Security Office"}},
|
|
{ID: "alma", Title: "Alma (WA Prep)", Path: []string{"Tricia", "School"}},
|
|
},
|
|
},
|
|
},
|
|
CurrentPath: []string{"Crew"},
|
|
}
|
|
|
|
got, err := state.ChildGroups()
|
|
if err != nil {
|
|
t.Fatalf("ChildGroups() error = %v", err)
|
|
}
|
|
|
|
if !slices.Equal(got, []string{"Internet", "Security Office"}) {
|
|
t.Fatalf("ChildGroups() = %v, want [Internet Security Office]", got)
|
|
}
|
|
}
|
|
|
|
func TestChildGroupsUsesTemplateSectionPaths(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Templates: []vault.Entry{
|
|
{ID: "tpl-1", Title: "Website Login", Path: []string{"Templates", "Web"}},
|
|
{ID: "tpl-2", Title: "SSH Login", Path: []string{"Templates", "Infra"}},
|
|
},
|
|
},
|
|
},
|
|
Section: SectionTemplates,
|
|
CurrentPath: []string{"Templates"},
|
|
}
|
|
|
|
got, err := state.ChildGroups()
|
|
if err != nil {
|
|
t.Fatalf("ChildGroups() error = %v", err)
|
|
}
|
|
|
|
if !slices.Equal(got, []string{"Infra", "Web"}) {
|
|
t.Fatalf("ChildGroups() = %v, want [Infra Web]", got)
|
|
}
|
|
}
|
|
|
|
func TestSelectVisibleEntryAndToggleSelection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Crew", "Internet"}},
|
|
},
|
|
},
|
|
},
|
|
CurrentPath: []string{"Crew", "Internet"},
|
|
}
|
|
|
|
if err := state.SelectVisibleIndex(1); err != nil {
|
|
t.Fatalf("SelectVisibleIndex() error = %v", err)
|
|
}
|
|
if got := state.SelectedEntryID; got != "vault-console" {
|
|
t.Fatalf("SelectedEntryID = %q, want %q", got, "vault-console")
|
|
}
|
|
|
|
if err := state.ToggleVisibleIndex(1); err != nil {
|
|
t.Fatalf("ToggleVisibleIndex() error = %v", err)
|
|
}
|
|
if got := state.SelectedEntryID; got != "" {
|
|
t.Fatalf("SelectedEntryID after toggle = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestVisibleEntriesFailsWhenVaultIsLocked(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Session: stubSession{err: session.ErrLocked},
|
|
}
|
|
|
|
_, err := state.VisibleEntries()
|
|
if !errors.Is(err, session.ErrLocked) {
|
|
t.Fatalf("VisibleEntries() error = %v, want ErrLocked", err)
|
|
}
|
|
}
|
|
|
|
type stubSession struct {
|
|
model vault.Model
|
|
err error
|
|
}
|
|
|
|
func (s stubSession) Current() (vault.Model, error) {
|
|
if s.err != nil {
|
|
return vault.Model{}, s.err
|
|
}
|
|
return s.model, nil
|
|
}
|
|
|
|
func TestDeleteSelectedEntryUpdatesSessionAndClearsSelection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
|
},
|
|
},
|
|
}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "vault-console",
|
|
}
|
|
|
|
if err := state.DeleteSelectedEntry(); err != nil {
|
|
t.Fatalf("DeleteSelectedEntry() error = %v", err)
|
|
}
|
|
|
|
if got := state.SelectedEntryID; got != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty", got)
|
|
}
|
|
|
|
if len(sess.model.Entries) != 0 {
|
|
t.Fatalf("len(Entries) = %d, want 0", len(sess.model.Entries))
|
|
}
|
|
|
|
if len(sess.model.RecycleBin) != 1 || sess.model.RecycleBin[0].ID != "vault-console" {
|
|
t.Fatalf("RecycleBin = %#v, want vault-console entry", sess.model.RecycleBin)
|
|
}
|
|
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after delete")
|
|
}
|
|
}
|
|
|
|
func TestRestoreEntryMovesEntryBackIntoVisibleEntries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{
|
|
model: vault.Model{
|
|
RecycleBin: []vault.Entry{
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
|
},
|
|
},
|
|
}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
}
|
|
|
|
if err := state.RestoreEntry("vault-console"); err != nil {
|
|
t.Fatalf("RestoreEntry() error = %v", err)
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
|
|
if len(got) != 1 || got[0].ID != "vault-console" {
|
|
t.Fatalf("VisibleEntries() = %#v, want restored vault-console entry", got)
|
|
}
|
|
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after restore")
|
|
}
|
|
}
|
|
|
|
func TestUpsertEntryPersistsEntryAndSelectsIt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{
|
|
model: vault.Model{},
|
|
}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
}
|
|
|
|
entry := vault.Entry{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Username: "dannyocean",
|
|
Password: "bellagio-pass-1",
|
|
URL: "https://vault.crew.example.invalid",
|
|
Path: []string{"Root", "Internet"},
|
|
}
|
|
if err := state.UpsertEntry(entry); err != nil {
|
|
t.Fatalf("UpsertEntry() error = %v", err)
|
|
}
|
|
|
|
if got := state.SelectedEntryID; got != "vault-console" {
|
|
t.Fatalf("SelectedEntryID = %q, want %q", got, "vault-console")
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].Password != "bellagio-pass-1" {
|
|
t.Fatalf("VisibleEntries() = %#v, want persisted vault-console entry", got)
|
|
}
|
|
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after upsert")
|
|
}
|
|
}
|
|
|
|
func TestInstantiateTemplateCreatesEntryAndSelectsIt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{
|
|
model: vault.Model{
|
|
Templates: []vault.Entry{
|
|
{
|
|
ID: "website-login",
|
|
Title: "Website Login",
|
|
Username: "template-user",
|
|
Password: "template-password",
|
|
URL: "https://example.com",
|
|
Notes: "Reusable template for website accounts.",
|
|
Path: []string{"Templates"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
}
|
|
|
|
entry, err := state.InstantiateTemplate("website-login", vault.Entry{
|
|
ID: "bellagio",
|
|
Title: "Bellagio",
|
|
Username: "rustyryan",
|
|
Password: "hunter2",
|
|
URL: "https://bellagio.example.invalid",
|
|
Path: []string{"Root", "Internet"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InstantiateTemplate() error = %v", err)
|
|
}
|
|
|
|
if entry.Notes != "Reusable template for website accounts." {
|
|
t.Fatalf("entry.Notes = %q, want template notes", entry.Notes)
|
|
}
|
|
|
|
if got := state.SelectedEntryID; got != "bellagio" {
|
|
t.Fatalf("SelectedEntryID = %q, want %q", got, "bellagio")
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].ID != "bellagio" {
|
|
t.Fatalf("VisibleEntries() = %#v, want instantiated bellagio entry", got)
|
|
}
|
|
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after template instantiation")
|
|
}
|
|
}
|
|
|
|
func TestUpsertTemplateCreatesTemplateAndMarksStateDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: vault.Model{}}
|
|
state := State{Session: sess}
|
|
|
|
if err := state.UpsertTemplate(vault.Entry{
|
|
ID: "tpl-1",
|
|
Title: "Website Login",
|
|
Username: "template-user",
|
|
Path: []string{"Templates"},
|
|
}); err != nil {
|
|
t.Fatalf("UpsertTemplate() error = %v", err)
|
|
}
|
|
|
|
if len(sess.model.Templates) != 1 || sess.model.Templates[0].ID != "tpl-1" {
|
|
t.Fatalf("Templates = %#v, want tpl-1 template", sess.model.Templates)
|
|
}
|
|
if state.SelectedEntryID != "tpl-1" {
|
|
t.Fatalf("SelectedEntryID = %q, want tpl-1 after template upsert", state.SelectedEntryID)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after template upsert")
|
|
}
|
|
}
|
|
|
|
func TestDeleteTemplateRemovesTemplateAndClearsSelection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: vault.Model{
|
|
Templates: []vault.Entry{
|
|
{ID: "tpl-1", Title: "Website Login", Path: []string{"Templates"}},
|
|
},
|
|
}}
|
|
state := State{
|
|
Session: sess,
|
|
SelectedEntryID: "tpl-1",
|
|
}
|
|
|
|
if err := state.DeleteTemplate("tpl-1"); err != nil {
|
|
t.Fatalf("DeleteTemplate() error = %v", err)
|
|
}
|
|
|
|
if len(sess.model.Templates) != 0 {
|
|
t.Fatalf("Templates = %#v, want empty after delete", sess.model.Templates)
|
|
}
|
|
if state.SelectedEntryID != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty after delete", state.SelectedEntryID)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after template delete")
|
|
}
|
|
}
|
|
|
|
func TestDuplicateSelectedEntryCreatesCopyAndSelectsIt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
|
},
|
|
}}
|
|
state := State{
|
|
Session: sess,
|
|
SelectedEntryID: "vault-console",
|
|
}
|
|
|
|
duplicate, err := state.DuplicateSelectedEntry("vault-console-copy")
|
|
if err != nil {
|
|
t.Fatalf("DuplicateSelectedEntry() error = %v", err)
|
|
}
|
|
|
|
if duplicate.ID != "vault-console-copy" {
|
|
t.Fatalf("duplicate.ID = %q, want %q", duplicate.ID, "vault-console-copy")
|
|
}
|
|
if state.SelectedEntryID != "vault-console-copy" {
|
|
t.Fatalf("SelectedEntryID = %q, want vault-console-copy", state.SelectedEntryID)
|
|
}
|
|
if len(sess.model.Entries) != 2 {
|
|
t.Fatalf("len(Entries) = %d, want 2 after duplicate", len(sess.model.Entries))
|
|
}
|
|
}
|
|
|
|
func TestMoveSelectedEntryMovesEntryToNewPathAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
|
},
|
|
}}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "vault-console",
|
|
}
|
|
|
|
if err := state.MoveSelectedEntry([]string{"Root", "Infrastructure"}); err != nil {
|
|
t.Fatalf("MoveSelectedEntry() error = %v", err)
|
|
}
|
|
|
|
oldPath := sess.model.EntriesInPath([]string{"Root", "Internet"})
|
|
if len(oldPath) != 0 {
|
|
t.Fatalf("EntriesInPath(Root/Internet) = %#v, want empty after move", oldPath)
|
|
}
|
|
|
|
newPath := sess.model.EntriesInPath([]string{"Root", "Infrastructure"})
|
|
if len(newPath) != 1 || newPath[0].ID != "vault-console" {
|
|
t.Fatalf("EntriesInPath(Root/Infrastructure) = %#v, want moved vault-console entry", newPath)
|
|
}
|
|
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after move")
|
|
}
|
|
}
|
|
|
|
func TestRestoreSelectedEntryVersionReplacesCurrentVersion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Password: "new-token",
|
|
Path: []string{"Root", "Internet"},
|
|
History: []vault.Entry{
|
|
{ID: "vault-console-h1", Title: "Vault Console", Password: "old-token", Path: []string{"Root", "Internet"}},
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
state := State{
|
|
Session: sess,
|
|
SelectedEntryID: "vault-console",
|
|
}
|
|
|
|
if err := state.RestoreSelectedEntryVersion(0); err != nil {
|
|
t.Fatalf("RestoreSelectedEntryVersion() error = %v", err)
|
|
}
|
|
|
|
if got := sess.model.Entries[0].Password; got != "old-token" {
|
|
t.Fatalf("Entries[0].Password = %q, want %q", got, "old-token")
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after history restore")
|
|
}
|
|
}
|
|
|
|
func TestSaveClearsDirtyState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &saveableStubSession{}
|
|
state := State{
|
|
Session: sess,
|
|
Dirty: true,
|
|
}
|
|
|
|
if err := state.Save(); err != nil {
|
|
t.Fatalf("Save() error = %v", err)
|
|
}
|
|
|
|
if state.Dirty {
|
|
t.Fatal("Dirty = true, want false after save")
|
|
}
|
|
|
|
if sess.saveCalls != 1 {
|
|
t.Fatalf("saveCalls = %d, want 1", sess.saveCalls)
|
|
}
|
|
}
|
|
|
|
func TestCreateVaultResetsSelectionPathAndDirtyState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &lifecycleStubSession{}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "vault-console",
|
|
Dirty: true,
|
|
}
|
|
|
|
if err := state.CreateVault(vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
|
|
t.Fatalf("CreateVault() error = %v", err)
|
|
}
|
|
|
|
if sess.createCalls != 1 {
|
|
t.Fatalf("createCalls = %d, want 1", sess.createCalls)
|
|
}
|
|
if len(state.CurrentPath) != 0 {
|
|
t.Fatalf("CurrentPath = %v, want empty", state.CurrentPath)
|
|
}
|
|
if state.SelectedEntryID != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
|
|
}
|
|
if state.Dirty {
|
|
t.Fatal("Dirty = true, want false after create")
|
|
}
|
|
}
|
|
|
|
func TestOpenVaultResetsSelectionPathAndDirtyState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &lifecycleStubSession{}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "vault-console",
|
|
Dirty: true,
|
|
}
|
|
|
|
if err := state.OpenVault("/tmp/test.kdbx", vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
|
|
t.Fatalf("OpenVault() error = %v", err)
|
|
}
|
|
|
|
if sess.openPath != "/tmp/test.kdbx" {
|
|
t.Fatalf("openPath = %q, want %q", sess.openPath, "/tmp/test.kdbx")
|
|
}
|
|
if len(state.CurrentPath) != 0 {
|
|
t.Fatalf("CurrentPath = %v, want empty", state.CurrentPath)
|
|
}
|
|
if state.SelectedEntryID != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
|
|
}
|
|
if state.Dirty {
|
|
t.Fatal("Dirty = true, want false after open")
|
|
}
|
|
}
|
|
|
|
func TestSaveAsClearsDirtyState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &lifecycleStubSession{}
|
|
state := State{
|
|
Session: sess,
|
|
Dirty: true,
|
|
}
|
|
|
|
if err := state.SaveAs("/tmp/other.kdbx"); err != nil {
|
|
t.Fatalf("SaveAs() error = %v", err)
|
|
}
|
|
|
|
if sess.saveAsPath != "/tmp/other.kdbx" {
|
|
t.Fatalf("saveAsPath = %q, want %q", sess.saveAsPath, "/tmp/other.kdbx")
|
|
}
|
|
if state.Dirty {
|
|
t.Fatal("Dirty = true, want false after save-as")
|
|
}
|
|
}
|
|
|
|
func TestOpenRemoteVaultResetsSelectionPathAndDirtyState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &lifecycleStubSession{}
|
|
client := webdav.Client{BaseURL: "https://example.com"}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "vault-console",
|
|
Dirty: true,
|
|
}
|
|
|
|
if err := state.OpenRemoteVault(client, "vaults/main.kdbx", vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
|
|
t.Fatalf("OpenRemoteVault() error = %v", err)
|
|
}
|
|
|
|
if sess.remotePath != "vaults/main.kdbx" {
|
|
t.Fatalf("remotePath = %q, want %q", sess.remotePath, "vaults/main.kdbx")
|
|
}
|
|
if len(state.CurrentPath) != 0 {
|
|
t.Fatalf("CurrentPath = %v, want empty", state.CurrentPath)
|
|
}
|
|
if state.SelectedEntryID != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
|
|
}
|
|
if state.Dirty {
|
|
t.Fatal("Dirty = true, want false after remote open")
|
|
}
|
|
}
|
|
|
|
func TestOpenBoundRemoteVaultResolvesClientFromVaultBinding(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &lifecycleStubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "remote-creds-1",
|
|
Title: "Bellagio WebDAV Sign-In",
|
|
Username: "linuscaldwell",
|
|
Password: "bellagio-pass-1",
|
|
Path: []string{"Crew", "Internet"},
|
|
},
|
|
},
|
|
RemoteProfiles: []vault.RemoteProfile{
|
|
{
|
|
ID: "bellagio-webdav",
|
|
Name: "Bellagio Vault",
|
|
Backend: vault.RemoteBackendWebDAV,
|
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
|
Path: "files/bellagio/keepass.kdbx",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "vault-console",
|
|
Dirty: true,
|
|
}
|
|
|
|
err := state.OpenBoundRemoteVault(RemoteBinding{
|
|
LocalVaultPath: "/tmp/bellagio.kdbx",
|
|
RemoteProfileID: "bellagio-webdav",
|
|
CredentialEntryID: "remote-creds-1",
|
|
SyncMode: SyncModeAutomaticOnOpenSave,
|
|
}, vault.MasterKey{Password: "correct horse battery staple"})
|
|
if err != nil {
|
|
t.Fatalf("OpenBoundRemoteVault() error = %v", err)
|
|
}
|
|
|
|
if got := sess.remoteClient.BaseURL; got != "https://dav.example.invalid/remote.php/dav" {
|
|
t.Fatalf("remote client base URL = %q, want remote.php/dav URL", got)
|
|
}
|
|
if got := sess.remoteClient.Username; got != "linuscaldwell" {
|
|
t.Fatalf("remote client username = %q, want linuscaldwell", got)
|
|
}
|
|
if got := sess.remoteClient.Password; got != "bellagio-pass-1" {
|
|
t.Fatalf("remote client password = %q, want bellagio-pass-1", got)
|
|
}
|
|
if got := sess.remotePath; got != "files/bellagio/keepass.kdbx" {
|
|
t.Fatalf("remotePath = %q, want files/bellagio/keepass.kdbx", got)
|
|
}
|
|
if len(state.CurrentPath) != 0 {
|
|
t.Fatalf("CurrentPath = %v, want empty", state.CurrentPath)
|
|
}
|
|
if state.SelectedEntryID != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
|
|
}
|
|
if state.Dirty {
|
|
t.Fatal("Dirty = true, want false after bound remote open")
|
|
}
|
|
}
|
|
|
|
func TestOpenBoundRemoteVaultReturnsResolutionErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &lifecycleStubSession{model: vault.Model{}}
|
|
state := State{Session: sess}
|
|
|
|
err := state.OpenBoundRemoteVault(RemoteBinding{
|
|
LocalVaultPath: "/tmp/bellagio.kdbx",
|
|
RemoteProfileID: "missing-profile",
|
|
CredentialEntryID: "remote-creds-1",
|
|
}, vault.MasterKey{Password: "correct horse battery staple"})
|
|
if !errors.Is(err, vault.ErrRemoteProfileNotFound) {
|
|
t.Fatalf("OpenBoundRemoteVault() error = %v, want ErrRemoteProfileNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestConfigureRemoteBindingPersistsIntoCurrentVaultAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: vault.Model{}}
|
|
state := State{Session: sess}
|
|
|
|
binding, err := state.ConfigureRemoteBinding(RemoteBindingInput{
|
|
LocalVaultPath: "/tmp/bellagio.kdbx",
|
|
RemoteProfileID: "bellagio-webdav",
|
|
RemoteProfileName: "Bellagio Vault",
|
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
|
RemotePath: "files/bellagio/keepass.kdbx",
|
|
CredentialEntryID: "remote-creds-1",
|
|
CredentialTitle: "Bellagio WebDAV Sign-In",
|
|
Username: "linuscaldwell",
|
|
Password: "bellagio-pass-1",
|
|
CredentialPath: []string{"Crew", "Internet"},
|
|
SyncMode: SyncModeAutomaticOnOpenSave,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ConfigureRemoteBinding() error = %v", err)
|
|
}
|
|
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after ConfigureRemoteBinding")
|
|
}
|
|
if got := binding.RemoteProfileID; got != "bellagio-webdav" {
|
|
t.Fatalf("binding.RemoteProfileID = %q, want bellagio-webdav", got)
|
|
}
|
|
if got := len(sess.model.RemoteProfiles); got != 1 {
|
|
t.Fatalf("len(RemoteProfiles) = %d, want 1", got)
|
|
}
|
|
credentials, err := sess.model.EntryByID("remote-creds-1")
|
|
if err != nil {
|
|
t.Fatalf("EntryByID(remote-creds-1) error = %v", err)
|
|
}
|
|
if credentials.Username != "linuscaldwell" || credentials.Password != "bellagio-pass-1" {
|
|
t.Fatalf("stored credential entry = %#v, want linuscaldwell/bellagio-pass-1", credentials)
|
|
}
|
|
}
|
|
|
|
func TestConfigureRemoteBindingRequiresMutableSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{Session: stubSession{model: vault.Model{}}}
|
|
_, err := state.ConfigureRemoteBinding(RemoteBindingInput{
|
|
LocalVaultPath: "/tmp/bellagio.kdbx",
|
|
RemoteProfileID: "bellagio-webdav",
|
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
|
RemotePath: "files/bellagio/keepass.kdbx",
|
|
CredentialEntryID: "remote-creds-1",
|
|
Password: "bellagio-pass-1",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("ConfigureRemoteBinding() error = nil, want mutability error")
|
|
}
|
|
}
|
|
|
|
func TestRemoveRemoteBindingRemovesVaultDataAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: vault.Model{
|
|
Entries: []vault.Entry{{
|
|
ID: "remote-creds-1",
|
|
Title: "Bellagio WebDAV Sign-In",
|
|
Username: "linuscaldwell",
|
|
Password: "bellagio-pass-1",
|
|
}},
|
|
RemoteProfiles: []vault.RemoteProfile{{
|
|
ID: "bellagio-webdav",
|
|
Name: "Bellagio Vault",
|
|
Backend: vault.RemoteBackendWebDAV,
|
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
|
Path: "files/bellagio/keepass.kdbx",
|
|
}},
|
|
}}
|
|
state := State{Session: sess}
|
|
|
|
err := state.RemoveRemoteBinding(RemoteBinding{
|
|
LocalVaultPath: "/tmp/bellagio.kdbx",
|
|
RemoteProfileID: "bellagio-webdav",
|
|
CredentialEntryID: "remote-creds-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RemoveRemoteBinding() error = %v", err)
|
|
}
|
|
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after RemoveRemoteBinding")
|
|
}
|
|
if got := len(sess.model.RemoteProfiles); got != 0 {
|
|
t.Fatalf("len(RemoteProfiles) = %d, want 0", got)
|
|
}
|
|
if _, err := sess.model.EntryByID("remote-creds-1"); !errors.Is(err, vault.ErrEntryNotFound) {
|
|
t.Fatalf("EntryByID(remote-creds-1) error = %v, want ErrEntryNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestLockClearsSelectionAndMakesVaultUnavailable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &lockableStubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
|
},
|
|
},
|
|
}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "vault-console",
|
|
}
|
|
|
|
if err := state.Lock(); err != nil {
|
|
t.Fatalf("Lock() error = %v", err)
|
|
}
|
|
|
|
if got := state.SelectedEntryID; got != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty after lock", got)
|
|
}
|
|
|
|
_, err := state.VisibleEntries()
|
|
if !errors.Is(err, session.ErrLocked) {
|
|
t.Fatalf("VisibleEntries() error = %v, want ErrLocked", err)
|
|
}
|
|
}
|
|
|
|
func TestUnlockRestoresVaultVisibility(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &lockableStubSession{
|
|
model: vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
|
},
|
|
},
|
|
locked: true,
|
|
}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
}
|
|
|
|
if err := state.Unlock(vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
|
|
t.Fatalf("Unlock() error = %v", err)
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].ID != "vault-console" {
|
|
t.Fatalf("VisibleEntries() = %#v, want vault-console entry after unlock", got)
|
|
}
|
|
}
|
|
|
|
func TestChangeMasterKeyMarksStateDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &lifecycleStubSession{}
|
|
state := State{Session: sess}
|
|
|
|
key := vault.MasterKey{Password: "correct horse battery staple"}
|
|
if err := state.ChangeMasterKey(key); err != nil {
|
|
t.Fatalf("ChangeMasterKey() error = %v", err)
|
|
}
|
|
|
|
if got := sess.changedKey; got.Password != key.Password {
|
|
t.Fatalf("changedKey = %#v, want %#v", got, key)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after ChangeMasterKey")
|
|
}
|
|
}
|
|
|
|
func TestShowSectionResetsPathAndSelection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
Section: SectionEntries,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "vault-console",
|
|
SearchQuery: "git",
|
|
}
|
|
|
|
state.ShowSection(SectionTemplates)
|
|
|
|
if state.Section != SectionTemplates {
|
|
t.Fatalf("Section = %q, want %q", state.Section, SectionTemplates)
|
|
}
|
|
if len(state.CurrentPath) != 0 {
|
|
t.Fatalf("CurrentPath = %v, want empty", state.CurrentPath)
|
|
}
|
|
if state.SelectedEntryID != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
|
|
}
|
|
if state.SearchQuery != "git" {
|
|
t.Fatalf("SearchQuery = %q, want search preserved", state.SearchQuery)
|
|
}
|
|
}
|
|
|
|
func TestSetSearchQueryUpdatesControllerSearchState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{}
|
|
|
|
state.SetSearchQuery("lights")
|
|
|
|
if state.SearchQuery != "lights" {
|
|
t.Fatalf("SearchQuery = %q, want %q", state.SearchQuery, "lights")
|
|
}
|
|
}
|
|
|
|
func TestBeginNewEntryClearsSelectionAndStatus(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
SelectedEntryID: "vault-console",
|
|
ErrorMessage: "previous error",
|
|
}
|
|
|
|
state.BeginNewEntry()
|
|
|
|
if state.SelectedEntryID != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
|
|
}
|
|
if state.StatusMessage != "" {
|
|
t.Fatalf("StatusMessage = %q, want empty", state.StatusMessage)
|
|
}
|
|
if state.ErrorMessage != "" {
|
|
t.Fatalf("ErrorMessage = %q, want empty", state.ErrorMessage)
|
|
}
|
|
}
|
|
|
|
func TestSetActionResultTracksSuccessAndFailureMessages(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{}
|
|
|
|
state.SetActionResult("save vault", nil)
|
|
if state.StatusMessage != "save vault complete" {
|
|
t.Fatalf("StatusMessage = %q, want save vault complete", state.StatusMessage)
|
|
}
|
|
if state.ErrorMessage != "" {
|
|
t.Fatalf("ErrorMessage = %q, want empty on success", state.ErrorMessage)
|
|
}
|
|
|
|
state.SetActionResult("save vault", errors.New("disk full"))
|
|
if state.StatusMessage != "" {
|
|
t.Fatalf("StatusMessage = %q, want empty on failure", state.StatusMessage)
|
|
}
|
|
if state.ErrorMessage != "disk full" {
|
|
t.Fatalf("ErrorMessage = %q, want disk full", state.ErrorMessage)
|
|
}
|
|
}
|
|
|
|
func TestEnterGroupAppendsPathAndClearsSelection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
CurrentPath: []string{"Root"},
|
|
SelectedEntryID: "vault-console",
|
|
}
|
|
|
|
state.EnterGroup("Internet")
|
|
|
|
if !slices.Equal(state.CurrentPath, []string{"Root", "Internet"}) {
|
|
t.Fatalf("CurrentPath = %v, want [Root Internet]", state.CurrentPath)
|
|
}
|
|
if got := state.SelectedEntryID; got != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestNavigateToPathReplacesPathAndClearsSelection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
state := State{
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "vault-console",
|
|
}
|
|
|
|
state.NavigateToPath([]string{"Root", "Security Office"})
|
|
|
|
if !slices.Equal(state.CurrentPath, []string{"Root", "Security Office"}) {
|
|
t.Fatalf("CurrentPath = %v, want [Root Security Office]", state.CurrentPath)
|
|
}
|
|
if got := state.SelectedEntryID; got != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestDeleteCurrentGroupMovesToParentAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
model := testVaultModel()
|
|
model.CreateGroup([]string{"Root"}, "Finance")
|
|
sess := &mutableStubSession{model: model}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Finance"},
|
|
}
|
|
|
|
if err := state.DeleteCurrentGroup(); err != nil {
|
|
t.Fatalf("DeleteCurrentGroup() error = %v", err)
|
|
}
|
|
|
|
if !slices.Equal(state.CurrentPath, []string{"Root"}) {
|
|
t.Fatalf("CurrentPath = %v, want [Root]", state.CurrentPath)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after DeleteCurrentGroup")
|
|
}
|
|
|
|
got, err := state.ChildGroups()
|
|
if err != nil {
|
|
t.Fatalf("ChildGroups() error = %v", err)
|
|
}
|
|
if !slices.Equal(got, []string{"Internet", "Security Office"}) {
|
|
t.Fatalf("ChildGroups() = %v, want [Internet Security Office]", got)
|
|
}
|
|
}
|
|
|
|
func TestCreateGroupPersistsGroupAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: testVaultModel()}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root"},
|
|
}
|
|
|
|
if err := state.CreateGroup("Finance"); err != nil {
|
|
t.Fatalf("CreateGroup() error = %v", err)
|
|
}
|
|
|
|
got, err := state.ChildGroups()
|
|
if err != nil {
|
|
t.Fatalf("ChildGroups() error = %v", err)
|
|
}
|
|
|
|
if !slices.Equal(got, []string{"Finance", "Internet", "Security Office"}) {
|
|
t.Fatalf("ChildGroups() = %v, want Finance, Internet, Security Office", got)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after CreateGroup")
|
|
}
|
|
}
|
|
|
|
func TestCreateGroupSupportsNestedGroupPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
session := &mutableStubSession{model: vault.Model{}}
|
|
state := State{
|
|
Session: session,
|
|
CurrentPath: []string{"Root"},
|
|
}
|
|
|
|
if err := state.CreateGroup("Infrastructure / Prod"); err != nil {
|
|
t.Fatalf("CreateGroup() error = %v", err)
|
|
}
|
|
|
|
if got := session.model.ChildGroups([]string{"Root"}); !slices.Equal(got, []string{"Infrastructure"}) {
|
|
t.Fatalf("ChildGroups(Root) = %v, want [Infrastructure]", got)
|
|
}
|
|
if got := session.model.ChildGroups([]string{"Root", "Infrastructure"}); !slices.Equal(got, []string{"Prod"}) {
|
|
t.Fatalf("ChildGroups(Root/Infrastructure) = %v, want [Prod]", got)
|
|
}
|
|
}
|
|
|
|
func TestRenameCurrentGroupUpdatesPathAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: testVaultModel()}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
}
|
|
|
|
if err := state.RenameCurrentGroup("Infra"); err != nil {
|
|
t.Fatalf("RenameCurrentGroup() error = %v", err)
|
|
}
|
|
|
|
if !slices.Equal(state.CurrentPath, []string{"Root", "Infra"}) {
|
|
t.Fatalf("CurrentPath = %v, want [Root Infra]", state.CurrentPath)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after RenameCurrentGroup")
|
|
}
|
|
}
|
|
|
|
func TestDeleteCurrentGroupRemovesItNavigatesToParentAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
model := testVaultModel()
|
|
model.CreateGroup([]string{"Root"}, "Finance")
|
|
sess := &mutableStubSession{model: model}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Finance"},
|
|
}
|
|
|
|
if err := state.DeleteCurrentGroup(); err != nil {
|
|
t.Fatalf("DeleteCurrentGroup() error = %v", err)
|
|
}
|
|
|
|
if !slices.Equal(state.CurrentPath, []string{"Root"}) {
|
|
t.Fatalf("CurrentPath = %v, want [Root]", state.CurrentPath)
|
|
}
|
|
|
|
got, err := state.ChildGroups()
|
|
if err != nil {
|
|
t.Fatalf("ChildGroups() error = %v", err)
|
|
}
|
|
|
|
if !slices.Equal(got, []string{"Internet", "Security Office"}) {
|
|
t.Fatalf("ChildGroups() = %v, want [Internet Security Office]", got)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after DeleteCurrentGroup")
|
|
}
|
|
}
|
|
|
|
func TestMoveSelectedEntryPersistsPathChangeAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: testVaultModel()}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "bellagio",
|
|
}
|
|
|
|
if err := state.MoveSelectedEntry([]string{"Root", "Security Office"}); err != nil {
|
|
t.Fatalf("MoveSelectedEntry() error = %v", err)
|
|
}
|
|
|
|
state.NavigateToPath([]string{"Root", "Security Office"})
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
|
|
if len(got) != 2 {
|
|
t.Fatalf("len(VisibleEntries()) = %d, want 2", len(got))
|
|
}
|
|
if got[0].ID != "bellagio" && got[1].ID != "bellagio" {
|
|
t.Fatalf("VisibleEntries() = %#v, want moved bellagio entry in destination group", got)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after MoveSelectedEntry")
|
|
}
|
|
}
|
|
|
|
func TestMoveCurrentGroupMovesHierarchyAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
model := vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "vault-console", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
|
},
|
|
}
|
|
model.CreateGroup([]string{"Root", "Internet"}, "Infrastructure")
|
|
|
|
session := &mutableStubSession{model: model}
|
|
state := State{
|
|
Session: session,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
}
|
|
|
|
if err := state.MoveCurrentGroup([]string{"Root", "Crew"}); err != nil {
|
|
t.Fatalf("MoveCurrentGroup() error = %v", err)
|
|
}
|
|
|
|
if !slices.Equal(state.CurrentPath, []string{"Root", "Crew", "Internet"}) {
|
|
t.Fatalf("CurrentPath = %v, want [Root Crew Internet]", state.CurrentPath)
|
|
}
|
|
if got := session.model.EntriesInPath([]string{"Root", "Crew", "Internet"}); len(got) != 1 || got[0].ID != "vault-console" {
|
|
t.Fatalf("EntriesInPath(Root/Crew/Internet) = %#v, want moved entry", got)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after MoveCurrentGroup")
|
|
}
|
|
}
|
|
|
|
func TestAddAttachmentToSelectedEntryPersistsAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: testVaultModel()}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "bellagio",
|
|
}
|
|
|
|
if err := state.AddAttachmentToSelectedEntry("token.txt", []byte("secret")); err != nil {
|
|
t.Fatalf("AddAttachmentToSelectedEntry() error = %v", err)
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
if string(got[0].Attachments["token.txt"]) != "secret" {
|
|
t.Fatalf("attachment content = %q, want %q", got[0].Attachments["token.txt"], "secret")
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after AddAttachmentToSelectedEntry")
|
|
}
|
|
}
|
|
|
|
func TestAddAttachmentToSelectedEntryRejectsDuplicateNames(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
model := testVaultModel()
|
|
model.Entries[0].Attachments = map[string][]byte{"token.txt": []byte("secret")}
|
|
sess := &mutableStubSession{model: model}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "bellagio",
|
|
}
|
|
|
|
err := state.AddAttachmentToSelectedEntry("token.txt", []byte("replacement"))
|
|
if !errors.Is(err, ErrAttachmentAlreadyExists) {
|
|
t.Fatalf("AddAttachmentToSelectedEntry() error = %v, want ErrAttachmentAlreadyExists", err)
|
|
}
|
|
|
|
got, currentErr := sess.Current()
|
|
if currentErr != nil {
|
|
t.Fatalf("Current() error = %v", currentErr)
|
|
}
|
|
if string(got.Entries[0].Attachments["token.txt"]) != "secret" {
|
|
t.Fatalf("attachment content = %q, want %q", got.Entries[0].Attachments["token.txt"], "secret")
|
|
}
|
|
}
|
|
|
|
func TestReplaceAttachmentOnSelectedEntryPersistsAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
model := testVaultModel()
|
|
model.Entries[0].Attachments = map[string][]byte{"token.txt": []byte("secret")}
|
|
sess := &mutableStubSession{model: model}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "bellagio",
|
|
}
|
|
|
|
if err := state.ReplaceAttachmentOnSelectedEntry("token.txt", []byte("replacement")); err != nil {
|
|
t.Fatalf("ReplaceAttachmentOnSelectedEntry() error = %v", err)
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
if string(got[0].Attachments["token.txt"]) != "replacement" {
|
|
t.Fatalf("attachment content = %q, want %q", got[0].Attachments["token.txt"], "replacement")
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after ReplaceAttachmentOnSelectedEntry")
|
|
}
|
|
}
|
|
|
|
func TestReplaceAttachmentOnSelectedEntryRequiresExistingAttachment(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: testVaultModel()}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "bellagio",
|
|
}
|
|
|
|
err := state.ReplaceAttachmentOnSelectedEntry("token.txt", []byte("replacement"))
|
|
if !errors.Is(err, ErrAttachmentNotFound) {
|
|
t.Fatalf("ReplaceAttachmentOnSelectedEntry() error = %v, want ErrAttachmentNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteAttachmentFromSelectedEntryPersistsAndMarksDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
model := testVaultModel()
|
|
model.Entries[0].Attachments = map[string][]byte{"token.txt": []byte("secret")}
|
|
sess := &mutableStubSession{model: model}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "bellagio",
|
|
}
|
|
|
|
if err := state.DeleteAttachmentFromSelectedEntry("token.txt"); err != nil {
|
|
t.Fatalf("DeleteAttachmentFromSelectedEntry() error = %v", err)
|
|
}
|
|
|
|
got, err := state.VisibleEntries()
|
|
if err != nil {
|
|
t.Fatalf("VisibleEntries() error = %v", err)
|
|
}
|
|
if len(got[0].Attachments) != 0 {
|
|
t.Fatalf("attachments = %#v, want empty", got[0].Attachments)
|
|
}
|
|
if !state.Dirty {
|
|
t.Fatal("Dirty = false, want true after DeleteAttachmentFromSelectedEntry")
|
|
}
|
|
}
|
|
|
|
func TestDeleteAttachmentFromSelectedEntryRequiresExistingAttachment(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sess := &mutableStubSession{model: testVaultModel()}
|
|
state := State{
|
|
Session: sess,
|
|
CurrentPath: []string{"Root", "Internet"},
|
|
SelectedEntryID: "bellagio",
|
|
}
|
|
|
|
err := state.DeleteAttachmentFromSelectedEntry("token.txt")
|
|
if !errors.Is(err, ErrAttachmentNotFound) {
|
|
t.Fatalf("DeleteAttachmentFromSelectedEntry() error = %v, want ErrAttachmentNotFound", err)
|
|
}
|
|
}
|
|
|
|
type mutableStubSession struct {
|
|
model vault.Model
|
|
err error
|
|
}
|
|
|
|
func (s *mutableStubSession) Current() (vault.Model, error) {
|
|
if s.err != nil {
|
|
return vault.Model{}, s.err
|
|
}
|
|
return s.model, nil
|
|
}
|
|
|
|
func (s *mutableStubSession) Replace(model vault.Model) {
|
|
s.model = model
|
|
}
|
|
|
|
func testVaultModel() vault.Model {
|
|
return vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "bellagio", Title: "Bellagio", Path: []string{"Root", "Internet"}},
|
|
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Root", "Security Office"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
type lockableStubSession struct {
|
|
model vault.Model
|
|
locked bool
|
|
}
|
|
|
|
func (s *lockableStubSession) Current() (vault.Model, error) {
|
|
if s.locked {
|
|
return vault.Model{}, session.ErrLocked
|
|
}
|
|
return s.model, nil
|
|
}
|
|
|
|
func (s *lockableStubSession) Lock() error {
|
|
s.locked = true
|
|
return nil
|
|
}
|
|
|
|
func (s *lockableStubSession) Unlock(vault.MasterKey) error {
|
|
s.locked = false
|
|
return nil
|
|
}
|
|
|
|
type saveableStubSession struct {
|
|
saveCalls int
|
|
}
|
|
|
|
func (s *saveableStubSession) Current() (vault.Model, error) {
|
|
return vault.Model{}, nil
|
|
}
|
|
|
|
func (s *saveableStubSession) Save() error {
|
|
s.saveCalls++
|
|
return nil
|
|
}
|
|
|
|
type lifecycleStubSession struct {
|
|
createCalls int
|
|
model vault.Model
|
|
openPath string
|
|
saveAsPath string
|
|
remoteClient webdav.Client
|
|
remotePath string
|
|
changedKey vault.MasterKey
|
|
}
|
|
|
|
func (s *lifecycleStubSession) Current() (vault.Model, error) {
|
|
return s.model, nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) Create(_ vault.Model, _ vault.MasterKey) error {
|
|
s.createCalls++
|
|
return nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) Open(path string, _ vault.MasterKey) error {
|
|
s.openPath = path
|
|
return nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) SaveAs(path string) error {
|
|
s.saveAsPath = path
|
|
return nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) OpenRemote(client webdav.Client, path string, _ vault.MasterKey) error {
|
|
s.remoteClient = client
|
|
s.remotePath = path
|
|
return nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) SynchronizeFromLocal(string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) SynchronizeFromLocalBytes(string, []byte) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) SynchronizeToLocal(string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) SynchronizeFromRemote(webdav.Client, string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) SynchronizeToRemote(webdav.Client, string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *lifecycleStubSession) ChangeMasterKey(key vault.MasterKey) error {
|
|
s.changedKey = key
|
|
return nil
|
|
}
|
|
|
|
type stubApprovalManager struct {
|
|
pending []apiapproval.Request
|
|
lastID string
|
|
lastOutcome apiapproval.Outcome
|
|
}
|
|
|
|
func (s stubApprovalManager) Pending() []apiapproval.Request {
|
|
return append([]apiapproval.Request(nil), s.pending...)
|
|
}
|
|
|
|
func (s *stubApprovalManager) Resolve(id string, outcome apiapproval.Outcome) (apiapproval.Request, *apitokens.PolicyRule, error) {
|
|
s.lastID = id
|
|
s.lastOutcome = outcome
|
|
return apiapproval.Request{ID: id}, nil, nil
|
|
}
|