Share local vaults from Android
This commit is contained in:
@@ -22,6 +22,11 @@
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/keepassgo_accessibility_service" />
|
||||
</service>
|
||||
<provider
|
||||
android:name="org.julianfamily.keepassgo.SharedVaultProvider"
|
||||
android:authorities="org.julianfamily.keepassgo.share"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
<activity
|
||||
android:name="org.julianfamily.keepassgo.SharedVaultImportActivity"
|
||||
android:exported="true"
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,208 @@
|
||||
//go:build android
|
||||
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -Werror
|
||||
#cgo LDFLAGS: -landroid
|
||||
|
||||
#include <jni.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static jint jni_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs) {
|
||||
return JNI_GetCreatedJavaVMs(vmBuf, bufLen, nVMs);
|
||||
}
|
||||
|
||||
static jint jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) {
|
||||
return (*vm)->AttachCurrentThread(vm, p_env, thr_args);
|
||||
}
|
||||
|
||||
static jint jni_DetachCurrentThread(JavaVM *vm) {
|
||||
return (*vm)->DetachCurrentThread(vm);
|
||||
}
|
||||
|
||||
static jclass jni_GetObjectClass(JNIEnv *env, jobject obj) {
|
||||
return (*env)->GetObjectClass(env, obj);
|
||||
}
|
||||
|
||||
static jmethodID jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
||||
return (*env)->GetMethodID(env, clazz, name, sig);
|
||||
}
|
||||
|
||||
static jmethodID jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
|
||||
return (*env)->GetStaticMethodID(env, clazz, name, sig);
|
||||
}
|
||||
|
||||
static jobject jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {
|
||||
return (*env)->CallObjectMethodA(env, obj, method, args);
|
||||
}
|
||||
|
||||
static void jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args) {
|
||||
(*env)->CallStaticVoidMethodA(env, cls, methodID, args);
|
||||
}
|
||||
|
||||
static jvalue jni_ValueObject(jobject obj) {
|
||||
jvalue value;
|
||||
value.l = obj;
|
||||
return value;
|
||||
}
|
||||
|
||||
static jthrowable jni_ExceptionOccurred(JNIEnv *env) {
|
||||
return (*env)->ExceptionOccurred(env);
|
||||
}
|
||||
|
||||
static void jni_ExceptionClear(JNIEnv *env) {
|
||||
(*env)->ExceptionClear(env);
|
||||
}
|
||||
|
||||
static jstring jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) {
|
||||
return (*env)->NewString(env, unicodeChars, len);
|
||||
}
|
||||
|
||||
static int jni_IsNull(jobject obj) {
|
||||
return obj == NULL;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"gioui.org/app"
|
||||
)
|
||||
|
||||
type androidVaultSharer struct{}
|
||||
|
||||
func newPlatformVaultSharer(goos string) vaultSharer {
|
||||
return androidVaultSharer{}
|
||||
}
|
||||
|
||||
func (androidVaultSharer) ShareVault(path, title string) error {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return fmt.Errorf("vault path is required")
|
||||
}
|
||||
ctx := C.jobject(unsafe.Pointer(app.AppContext()))
|
||||
if C.jni_IsNull(ctx) != 0 {
|
||||
return fmt.Errorf("android app context is not available")
|
||||
}
|
||||
vm, err := androidJavaVM()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var env *C.JNIEnv
|
||||
if res := C.jni_AttachCurrentThread(vm, &env, nil); res != C.JNI_OK {
|
||||
return fmt.Errorf("attach JVM thread: %d", int(res))
|
||||
}
|
||||
defer C.jni_DetachCurrentThread(vm)
|
||||
|
||||
sharerClass, err := androidLoadClass(env, ctx, "org.julianfamily.keepassgo.AndroidShare")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
methodName := cString("shareVault")
|
||||
defer C.free(unsafe.Pointer(methodName))
|
||||
methodSig := cString("(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)V")
|
||||
defer C.free(unsafe.Pointer(methodSig))
|
||||
method := C.jni_GetStaticMethodID(env, sharerClass, methodName, methodSig)
|
||||
if method == nil {
|
||||
if err := androidJNIError(env, "resolve shareVault method"); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("resolve shareVault method")
|
||||
}
|
||||
|
||||
jPath := androidJavaString(env, path)
|
||||
jTitle := androidJavaString(env, title)
|
||||
args := [3]C.jvalue{}
|
||||
args[0] = C.jni_ValueObject(ctx)
|
||||
args[1] = C.jni_ValueObject(C.jobject(jPath))
|
||||
args[2] = C.jni_ValueObject(C.jobject(jTitle))
|
||||
C.jni_CallStaticVoidMethodA(env, sharerClass, method, &args[0])
|
||||
if err := androidJNIError(env, "share vault"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func androidJavaVM() (*C.JavaVM, error) {
|
||||
var vms [1]*C.JavaVM
|
||||
var count C.jsize
|
||||
if res := C.jni_GetCreatedJavaVMs(&vms[0], 1, &count); res != C.JNI_OK {
|
||||
return nil, fmt.Errorf("get created Java VMs: %d", int(res))
|
||||
}
|
||||
if count == 0 || vms[0] == nil {
|
||||
return nil, fmt.Errorf("no Java VM available")
|
||||
}
|
||||
return vms[0], nil
|
||||
}
|
||||
|
||||
func androidLoadClass(env *C.JNIEnv, ctx C.jobject, name string) (C.jclass, error) {
|
||||
var zeroClass C.jclass
|
||||
contextClass := C.jni_GetObjectClass(env, ctx)
|
||||
getClassLoaderName := cString("getClassLoader")
|
||||
defer C.free(unsafe.Pointer(getClassLoaderName))
|
||||
getClassLoaderSig := cString("()Ljava/lang/ClassLoader;")
|
||||
defer C.free(unsafe.Pointer(getClassLoaderSig))
|
||||
getClassLoader := C.jni_GetMethodID(env, contextClass, getClassLoaderName, getClassLoaderSig)
|
||||
if getClassLoader == nil {
|
||||
if err := androidJNIError(env, "resolve getClassLoader"); err != nil {
|
||||
return zeroClass, err
|
||||
}
|
||||
return zeroClass, fmt.Errorf("resolve getClassLoader")
|
||||
}
|
||||
classLoader := C.jni_CallObjectMethodA(env, ctx, getClassLoader, nil)
|
||||
if err := androidJNIError(env, "load class loader"); err != nil {
|
||||
return zeroClass, err
|
||||
}
|
||||
if C.jni_IsNull(classLoader) != 0 {
|
||||
return zeroClass, fmt.Errorf("android class loader is nil")
|
||||
}
|
||||
|
||||
classLoaderClass := C.jni_GetObjectClass(env, classLoader)
|
||||
loadClassName := cString("loadClass")
|
||||
defer C.free(unsafe.Pointer(loadClassName))
|
||||
loadClassSig := cString("(Ljava/lang/String;)Ljava/lang/Class;")
|
||||
defer C.free(unsafe.Pointer(loadClassSig))
|
||||
loadClass := C.jni_GetMethodID(env, classLoaderClass, loadClassName, loadClassSig)
|
||||
if loadClass == nil {
|
||||
if err := androidJNIError(env, "resolve loadClass"); err != nil {
|
||||
return zeroClass, err
|
||||
}
|
||||
return zeroClass, fmt.Errorf("resolve loadClass")
|
||||
}
|
||||
|
||||
jClassName := androidJavaString(env, name)
|
||||
args := [1]C.jvalue{}
|
||||
args[0] = C.jni_ValueObject(C.jobject(jClassName))
|
||||
loaded := C.jni_CallObjectMethodA(env, classLoader, loadClass, &args[0])
|
||||
if err := androidJNIError(env, "load AndroidShare class"); err != nil {
|
||||
return zeroClass, err
|
||||
}
|
||||
if C.jni_IsNull(loaded) != 0 {
|
||||
return zeroClass, fmt.Errorf("load AndroidShare class returned nil")
|
||||
}
|
||||
return C.jclass(loaded), nil
|
||||
}
|
||||
|
||||
func androidJNIError(env *C.JNIEnv, action string) error {
|
||||
if thr := C.jni_ExceptionOccurred(env); C.jni_IsNull(C.jobject(thr)) == 0 {
|
||||
C.jni_ExceptionClear(env)
|
||||
return fmt.Errorf("%s: Java exception", action)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func androidJavaString(env *C.JNIEnv, s string) C.jstring {
|
||||
chars := utf16.Encode([]rune(s))
|
||||
if len(chars) == 0 {
|
||||
return C.jni_NewString(env, nil, 0)
|
||||
}
|
||||
return C.jni_NewString(env, (*C.jchar)(unsafe.Pointer(unsafe.SliceData(chars))), C.jsize(len(chars)))
|
||||
}
|
||||
|
||||
func cString(value string) *C.char {
|
||||
return C.CString(value)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !android
|
||||
|
||||
package main
|
||||
|
||||
func newPlatformVaultSharer(goos string) vaultSharer {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public final class AndroidShare {
|
||||
private static final String DEFAULT_TITLE = "KeePassGO Vault";
|
||||
|
||||
private AndroidShare() {
|
||||
}
|
||||
|
||||
public static void shareVault(Context context, String path, String title) throws IOException {
|
||||
File source = new File(path);
|
||||
if (!source.isFile()) {
|
||||
throw new IOException("vault file not found: " + path);
|
||||
}
|
||||
File shared = copyToSharedExport(context, source);
|
||||
Uri uri = SharedVaultProvider.uriForFile(shared.getName());
|
||||
|
||||
Intent send = new Intent(Intent.ACTION_SEND);
|
||||
send.setType("application/x-keepass2");
|
||||
send.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
send.putExtra(Intent.EXTRA_TITLE, sanitizeTitle(title, source.getName()));
|
||||
send.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
Intent chooser = Intent.createChooser(send, "Share vault");
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
chooser.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
context.startActivity(chooser);
|
||||
}
|
||||
|
||||
static File sharedDirectory(Context context) {
|
||||
return new File(new File(context.getFilesDir(), "keepassgo"), "shared");
|
||||
}
|
||||
|
||||
private static File copyToSharedExport(Context context, File source) throws IOException {
|
||||
File dir = sharedDirectory(context);
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
throw new IOException("failed to create " + dir.getAbsolutePath());
|
||||
}
|
||||
File target = new File(dir, sanitizeFilename(source.getName()));
|
||||
try (FileInputStream in = new FileInputStream(source);
|
||||
FileOutputStream out = new FileOutputStream(target, false)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int count;
|
||||
while ((count = in.read(buffer)) >= 0) {
|
||||
out.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private static String sanitizeFilename(String name) {
|
||||
String trimmed = name == null ? "" : name.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return "shared-vault.kdbx";
|
||||
}
|
||||
if (trimmed.endsWith(".kdbx")) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed + ".kdbx";
|
||||
}
|
||||
|
||||
private static String sanitizeTitle(String title, String fallbackName) {
|
||||
String trimmed = title == null ? "" : title.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
return trimmed;
|
||||
}
|
||||
String fallback = fallbackName == null ? "" : fallbackName.trim();
|
||||
if (!fallback.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
return DEFAULT_TITLE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
public final class SharedVaultProvider extends ContentProvider {
|
||||
private static final String AUTHORITY = "org.julianfamily.keepassgo.share";
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
File file = resolveSharedFile(uri);
|
||||
String[] columns = projection;
|
||||
if (columns == null || columns.length == 0) {
|
||||
columns = new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
|
||||
}
|
||||
MatrixCursor cursor = new MatrixCursor(columns, 1);
|
||||
Object[] row = new Object[columns.length];
|
||||
for (int i = 0; i < columns.length; i++) {
|
||||
switch (columns[i]) {
|
||||
case OpenableColumns.DISPLAY_NAME:
|
||||
row[i] = file.getName();
|
||||
break;
|
||||
case OpenableColumns.SIZE:
|
||||
row[i] = file.length();
|
||||
break;
|
||||
default:
|
||||
row[i] = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cursor.addRow(row);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return "application/x-keepass2";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
throw new UnsupportedOperationException("insert is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("delete is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("update is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
||||
File file = resolveSharedFile(uri);
|
||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
|
||||
static Uri uriForFile(String name) {
|
||||
return new Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(AUTHORITY)
|
||||
.appendPath(name)
|
||||
.build();
|
||||
}
|
||||
|
||||
private File resolveSharedFile(Uri uri) {
|
||||
if (getContext() == null) {
|
||||
throw new IllegalStateException("provider context is unavailable");
|
||||
}
|
||||
String name = sanitizeFilename(uri.getLastPathSegment());
|
||||
return new File(AndroidShare.sharedDirectory(getContext()), name);
|
||||
}
|
||||
|
||||
private static String sanitizeFilename(String name) {
|
||||
if (name == null) {
|
||||
return "shared-vault.kdbx";
|
||||
}
|
||||
String trimmed = name.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return "shared-vault.kdbx";
|
||||
}
|
||||
return new File(trimmed).getName();
|
||||
}
|
||||
}
|
||||
@@ -307,6 +307,7 @@ type ui struct {
|
||||
cancelEdit widget.Clickable
|
||||
pickVaultPath widget.Clickable
|
||||
importSharedVault widget.Clickable
|
||||
shareCurrentVault widget.Clickable
|
||||
pickKeyFile widget.Clickable
|
||||
pickSyncLocalPath widget.Clickable
|
||||
clearVaultSelection widget.Clickable
|
||||
@@ -426,6 +427,7 @@ type ui struct {
|
||||
settingsIcon *widget.Icon
|
||||
menuIcon *widget.Icon
|
||||
clipboardWriter clipboard.Writer
|
||||
vaultSharer vaultSharer
|
||||
loadingMessage string
|
||||
loadingActionLabel string
|
||||
lifecycleMode string
|
||||
@@ -497,6 +499,10 @@ type backgroundActionResult struct {
|
||||
id int
|
||||
}
|
||||
|
||||
type vaultSharer interface {
|
||||
ShareVault(path, title string) error
|
||||
}
|
||||
|
||||
var (
|
||||
bgColor = color.NRGBA{R: 242, G: 239, B: 233, A: 255}
|
||||
panelColor = color.NRGBA{R: 250, G: 248, B: 244, A: 255}
|
||||
@@ -635,6 +641,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
syncDefaultDirection: syncDirectionPull,
|
||||
apiPolicyGroupScope: true,
|
||||
autofillNoticePreference: autofillNoticeAll,
|
||||
vaultSharer: newPlatformVaultSharer(runtime.GOOS),
|
||||
backgroundResults: make(chan backgroundActionResult, 8),
|
||||
phoneGroupBrowserExpanded: true,
|
||||
}
|
||||
@@ -772,6 +779,10 @@ func supportsSharedVaultImport(goos string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(goos), "android")
|
||||
}
|
||||
|
||||
func supportsVaultShare(goos string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(goos), "android")
|
||||
}
|
||||
|
||||
func (u *ui) selectedAttachmentItems() []attachmentItem {
|
||||
item, ok := u.selectedEntry()
|
||||
if !ok || len(item.Attachments) == 0 {
|
||||
@@ -1551,6 +1562,24 @@ func (u *ui) importSharedVaultBytesAction(name string, content []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) currentShareableVaultPath() string {
|
||||
return strings.TrimSpace(u.vaultPath.Text())
|
||||
}
|
||||
|
||||
func (u *ui) shareCurrentVaultAction() error {
|
||||
if u.vaultSharer == nil {
|
||||
return fmt.Errorf("vault sharing is not available on this platform")
|
||||
}
|
||||
path := u.currentShareableVaultPath()
|
||||
if path == "" {
|
||||
return errors.New(errVaultPathRequired)
|
||||
}
|
||||
if err := u.state.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
return u.vaultSharer.ShareVault(path, friendlyRecentVaultLabel(path))
|
||||
}
|
||||
|
||||
func (u *ui) noteRecentVault(path string) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
@@ -3653,6 +3682,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
for u.saveCurrentRemoteBinding.Clicked(gtx) {
|
||||
u.runAction("save remote binding", u.saveCurrentRemoteBindingAction)
|
||||
}
|
||||
for u.shareCurrentVault.Clicked(gtx) {
|
||||
u.runAction("share vault", u.shareCurrentVaultAction)
|
||||
}
|
||||
for u.clearVaultSelection.Clicked(gtx) {
|
||||
if u.lifecycleBusy() {
|
||||
continue
|
||||
@@ -4777,6 +4809,17 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !supportsVaultShare(runtime.GOOS) || u.vaultSharer == nil || strings.TrimSpace(u.currentShareableVaultPath()) == "" {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.shareCurrentVault, "Share Vault")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return tonedButton(gtx, u.theme, &u.openAdvancedSync, "Open Advanced Sync")
|
||||
}),
|
||||
|
||||
+84
-1
@@ -99,6 +99,33 @@ func (s *remoteOpenCaptureSession) OpenRemote(client webdav.Client, path string,
|
||||
return nil
|
||||
}
|
||||
|
||||
type saveCaptureSession struct {
|
||||
model vault.Model
|
||||
saveCount int
|
||||
saveErr error
|
||||
}
|
||||
|
||||
func (s *saveCaptureSession) Current() (vault.Model, error) {
|
||||
return s.model, nil
|
||||
}
|
||||
|
||||
func (s *saveCaptureSession) Save() error {
|
||||
s.saveCount++
|
||||
return s.saveErr
|
||||
}
|
||||
|
||||
type captureVaultSharer struct {
|
||||
path string
|
||||
title string
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *captureVaultSharer) ShareVault(path, title string) error {
|
||||
s.path = path
|
||||
s.title = title
|
||||
return s.err
|
||||
}
|
||||
|
||||
func TestUIFiltersUsingVaultModelPathsAndSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -367,7 +394,12 @@ func TestUIChildGroupsComeFromVaultModel(t *testing.T) {
|
||||
func TestUIAPITokenLifecycleManagement(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("desktop", &session.Manager{})
|
||||
u := newUIWithSession("desktop", &session.Manager{}, statePaths{
|
||||
DefaultSaveAsPath: filepath.Join(t.TempDir(), "vault.kdbx"),
|
||||
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
|
||||
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
|
||||
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-prefs.json"),
|
||||
})
|
||||
u.masterPassword.SetText("correct horse battery staple")
|
||||
if err := u.createVaultAction(); err != nil {
|
||||
t.Fatalf("createVaultAction() error = %v", err)
|
||||
@@ -5787,6 +5819,57 @@ func TestUIConsumesPendingSharedVaultImportOnStartup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUICurrentShareableVaultPathUsesSelectedVaultPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("phone", &session.Manager{})
|
||||
u.vaultPath.SetText("/vaults/crew-shared.kdbx")
|
||||
|
||||
if got := u.currentShareableVaultPath(); got != "/vaults/crew-shared.kdbx" {
|
||||
t.Fatalf("currentShareableVaultPath() = %q, want %q", got, "/vaults/crew-shared.kdbx")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIShareCurrentVaultActionSavesAndSharesCurrentVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
session := &saveCaptureSession{}
|
||||
sharer := &captureVaultSharer{}
|
||||
u := newUIWithSession("phone", session)
|
||||
u.vaultSharer = sharer
|
||||
u.vaultPath.SetText("/vaults/crew-shared.kdbx")
|
||||
|
||||
if err := u.shareCurrentVaultAction(); err != nil {
|
||||
t.Fatalf("shareCurrentVaultAction() error = %v", err)
|
||||
}
|
||||
if session.saveCount != 1 {
|
||||
t.Fatalf("shareCurrentVaultAction() saveCount = %d, want 1", session.saveCount)
|
||||
}
|
||||
if got := sharer.path; got != "/vaults/crew-shared.kdbx" {
|
||||
t.Fatalf("ShareVault path = %q, want %q", got, "/vaults/crew-shared.kdbx")
|
||||
}
|
||||
if got := sharer.title; got != "crew-shared.kdbx" {
|
||||
t.Fatalf("ShareVault title = %q, want %q", got, "crew-shared.kdbx")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIShareCurrentVaultActionRequiresVaultPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithSession("phone", &saveCaptureSession{}, statePaths{
|
||||
DefaultSaveAsPath: filepath.Join(t.TempDir(), "vault.kdbx"),
|
||||
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
|
||||
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
|
||||
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-prefs.json"),
|
||||
})
|
||||
u.vaultSharer = &captureVaultSharer{}
|
||||
|
||||
err := u.shareCurrentVaultAction()
|
||||
if err == nil || err.Error() != errVaultPathRequired {
|
||||
t.Fatalf("shareCurrentVaultAction() error = %v, want %q", err, errVaultPathRequired)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultStatePathsUsesEnvironmentStateDirWhenFlagUnset(t *testing.T) {
|
||||
base := filepath.Join(t.TempDir(), "keepassgo-state-env")
|
||||
t.Setenv("KEEPASSGO_STATE_DIR", base)
|
||||
|
||||
Reference in New Issue
Block a user