Files
wehub-resource-sync bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / deadcode (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

304 lines
8.5 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package apps
import (
"os"
"path/filepath"
"runtime"
"sort"
"testing"
)
// storageTempDir points GetConfigDir at an isolated temp dir for the test.
func storageTempDir(t *testing.T) string {
t.Helper()
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
return dir
}
func TestStorageWriteReadRoundTrip(t *testing.T) {
storageTempDir(t)
want := []byte(`{"username":"u","token":"t"}`)
if err := Write("app_a", "git.json", want); err != nil {
t.Fatalf("Write: %v", err)
}
got, err := Read("app_a", "git.json")
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(got) != string(want) {
t.Fatalf("got %q, want %q", got, want)
}
}
func TestStorageReadMissingReturnsNil(t *testing.T) {
storageTempDir(t)
got, err := Read("app_a", "nope")
if err != nil {
t.Fatalf("err: %v", err)
}
if got != nil {
t.Fatalf("want nil, got %q", got)
}
}
func TestStorageEmptyArgsRejected(t *testing.T) {
storageTempDir(t)
if _, err := Read("", "k"); err == nil {
t.Error("Read empty appID should error")
}
if _, err := Read("a", ""); err == nil {
t.Error("Read empty key should error")
}
if err := Write("", "k", nil); err == nil {
t.Error("Write empty appID should error")
}
if err := Write("a", "", nil); err == nil {
t.Error("Write empty key should error")
}
if err := Delete("", "k"); err == nil {
t.Error("Delete empty appID should error")
}
if _, err := List(""); err == nil {
t.Error("List empty appID should error")
}
}
func TestStorageOverwrite(t *testing.T) {
storageTempDir(t)
if err := Write("app_a", "git.json", []byte("v1")); err != nil {
t.Fatalf("Write1: %v", err)
}
if err := Write("app_a", "git.json", []byte("v2")); err != nil {
t.Fatalf("Write2: %v", err)
}
got, _ := Read("app_a", "git.json")
if string(got) != "v2" {
t.Errorf("want v2, got %q", got)
}
}
func TestStorageDeleteIdempotent(t *testing.T) {
storageTempDir(t)
if err := Write("app_a", "git.json", []byte("x")); err != nil {
t.Fatalf("Write: %v", err)
}
if err := Delete("app_a", "git.json"); err != nil {
t.Fatalf("first Delete: %v", err)
}
if got, _ := Read("app_a", "git.json"); got != nil {
t.Error("file should be gone after Delete")
}
if err := Delete("app_a", "git.json"); err != nil {
t.Errorf("second Delete should be nil (idempotent), got %v", err)
}
}
func TestStorageListKeys(t *testing.T) {
storageTempDir(t)
for _, k := range []string{"git.json", "meta.json", "notes"} {
if err := Write("app_a", k, []byte("x")); err != nil {
t.Fatalf("Write %s: %v", k, err)
}
}
got, err := List("app_a")
if err != nil {
t.Fatalf("List: %v", err)
}
sort.Strings(got)
want := []string{"git.json", "meta.json", "notes"}
if len(got) != len(want) {
t.Fatalf("got %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("got %v, want %v", got, want)
}
}
}
func TestStorageListMissingAppDir(t *testing.T) {
storageTempDir(t)
got, err := List("never_written")
if err != nil {
t.Fatalf("List: %v", err)
}
if len(got) != 0 {
t.Errorf("want empty, got %v", got)
}
}
func TestStorageListSkipsSubdirs(t *testing.T) {
dir := storageTempDir(t)
if err := Write("app_a", "git.json", []byte("x")); err != nil {
t.Fatalf("Write: %v", err)
}
if err := os.Mkdir(filepath.Join(dir, "spark", "app_a", "sub"), 0700); err != nil {
t.Fatalf("mkdir: %v", err)
}
got, err := List("app_a")
if err != nil {
t.Fatalf("List: %v", err)
}
if len(got) != 1 || got[0] != "git.json" {
t.Errorf("want [git.json], got %v", got)
}
}
func TestStorageListSkipsInvalidDecodedKeys(t *testing.T) {
dir := storageTempDir(t)
if err := Write("app_a", "git.json", []byte("x")); err != nil {
t.Fatalf("Write: %v", err)
}
for _, name := range []string{"%zz", "%2E", "%2E%2E", "bad%2F..%2Fkey"} {
if err := os.WriteFile(filepath.Join(dir, "spark", "app_a", name), []byte("x"), 0600); err != nil {
t.Fatalf("write polluted key %s: %v", name, err)
}
}
got, err := List("app_a")
if err != nil {
t.Fatalf("List: %v", err)
}
if len(got) != 1 || got[0] != "git.json" {
t.Errorf("want [git.json], got %v", got)
}
}
func TestStorageEscapesAppIDAndKey(t *testing.T) {
dir := storageTempDir(t)
const appID, key = "a/b", "x/y"
if err := Write(appID, key, []byte("v")); err != nil {
t.Fatalf("Write: %v", err)
}
// no path traversal: spark/ has exactly one (escaped) app dir, no nested a/b tree
entries, _ := os.ReadDir(filepath.Join(dir, "spark"))
if len(entries) != 1 {
t.Fatalf("expected 1 escaped app dir under spark/, got %v", entries)
}
got, err := Read(appID, key)
if err != nil || string(got) != "v" {
t.Fatalf("Read escaped: got %q err %v", got, err)
}
keys, err := List(appID)
if err != nil || len(keys) != 1 || keys[0] != key {
t.Fatalf("List escaped: got %v err %v", keys, err)
}
}
func TestStorageRejectsTraversal(t *testing.T) {
dir := storageTempDir(t)
for _, bad := range []string{"..", ".", "../x", "a/../b"} {
if err := Write(bad, "k", []byte("x")); err == nil {
t.Errorf("Write appID=%q should error", bad)
}
if err := Write("app", bad, []byte("x")); err == nil {
t.Errorf("Write key=%q should error", bad)
}
if _, err := Read(bad, "k"); err == nil {
t.Errorf("Read appID=%q should error", bad)
}
if err := Delete(bad, "k"); err == nil {
t.Errorf("Delete appID=%q should error", bad)
}
if _, err := List(bad); err == nil {
t.Errorf("List appID=%q should error", bad)
}
}
// nothing escaped out of spark/ into ~/.lark-cli
if _, err := os.Stat(filepath.Join(dir, "k")); !os.IsNotExist(err) {
t.Error("traversal must not create files outside spark/")
}
}
func TestStorageReadNonNotExistError(t *testing.T) {
dir := storageTempDir(t)
// A directory at the key path makes ReadFile fail with a non-ErrNotExist error.
if err := os.MkdirAll(filepath.Join(dir, "spark", "app_a", "git.json"), 0700); err != nil {
t.Fatalf("mkdir key path: %v", err)
}
if _, err := Read("app_a", "git.json"); err == nil {
t.Fatal("expected error reading a directory key path")
}
}
func TestStorageWriteMkdirError(t *testing.T) {
dir := storageTempDir(t)
// A file at spark/ makes creating the per-app directory under it fail.
if err := os.WriteFile(filepath.Join(dir, "spark"), []byte("x"), 0600); err != nil {
t.Fatalf("write spark file: %v", err)
}
if err := Write("app_a", "git.json", []byte("x")); err == nil {
t.Fatal("expected mkdir error when spark/ is a file")
}
}
func TestStorageWriteAtomicError(t *testing.T) {
dir := storageTempDir(t)
// A directory at the key path makes the atomic write/rename over it fail.
if err := os.MkdirAll(filepath.Join(dir, "spark", "app_a", "git.json"), 0700); err != nil {
t.Fatalf("mkdir key path: %v", err)
}
if err := Write("app_a", "git.json", []byte("x")); err == nil {
t.Fatal("expected atomic write error when key path is a directory")
}
}
func TestStorageDeleteInvalidKey(t *testing.T) {
storageTempDir(t)
if err := Delete("app_a", ".."); err == nil {
t.Fatal("expected error deleting an invalid key")
}
}
func TestStorageDeleteRemoveError(t *testing.T) {
dir := storageTempDir(t)
// A non-empty directory at the key path makes Remove fail (non-ErrNotExist).
if err := os.MkdirAll(filepath.Join(dir, "spark", "app_a", "git.json", "child"), 0700); err != nil {
t.Fatalf("mkdir key path: %v", err)
}
if err := Delete("app_a", "git.json"); err == nil {
t.Fatal("expected error removing a non-empty directory key path")
}
}
func TestStorageListReadDirError(t *testing.T) {
dir := storageTempDir(t)
// A file at the per-app directory path makes ReadDir fail (non-ErrNotExist).
if err := os.MkdirAll(filepath.Join(dir, "spark"), 0700); err != nil {
t.Fatalf("mkdir spark: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "spark", "app_a"), []byte("x"), 0600); err != nil {
t.Fatalf("write app file: %v", err)
}
if _, err := List("app_a"); err == nil {
t.Fatal("expected error listing a file app directory")
}
}
func TestStoragePermsAndDir(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("perm bits not meaningful on windows")
}
dir := storageTempDir(t)
if err := Write("app_a", "git.json", []byte("x")); err != nil {
t.Fatalf("Write: %v", err)
}
fi, err := os.Stat(filepath.Join(dir, "spark", "app_a", "git.json"))
if err != nil {
t.Fatalf("stat file: %v", err)
}
if fi.Mode().Perm() != 0600 {
t.Errorf("file perm = %o, want 0600", fi.Mode().Perm())
}
di, err := os.Stat(filepath.Join(dir, "spark", "app_a"))
if err != nil {
t.Fatalf("stat dir: %v", err)
}
if di.Mode().Perm() != 0700 {
t.Errorf("dir perm = %o, want 0700", di.Mode().Perm())
}
}