mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
Compare commits
3 Commits
2229affe69
...
83acf00d85
| Author | SHA1 | Date | |
|---|---|---|---|
| 83acf00d85 | |||
| 46c2baf726 | |||
| a56d349da4 |
110
TEST_PLAN.md
Normal file
110
TEST_PLAN.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Test Coverage Plan
|
||||
|
||||
## Current State
|
||||
|
||||
- 63 tests, all passing (added 3 `render_table` tests)
|
||||
- Strong coverage: crypto (100%), ssh (80%), scan, features
|
||||
- `render_table` now takes `io.Writer` (Tier 1 item 1 done)
|
||||
- Misleading test files: `cmd_check_test`, `cmd_list_test`, `cmd_nushell_completion_test` don't test their namesake procs
|
||||
- Biggest gap: `db.odin` (15/21 procs untested), all `cmd_*` handlers untested, `parse_args` untested
|
||||
|
||||
## Tier 1 — Easy wins (pure functions, minimal setup)
|
||||
|
||||
### 1. `render_table` (table.odin)
|
||||
- Follow existing `render_json_rows` test pattern
|
||||
- Test cases: normal data (verify box-drawing chars, column alignment), empty rows, wide unicode, single column
|
||||
- Assert against `strings.Builder` output
|
||||
|
||||
### 2. `parse_args` (cli.odin) — BLOCKED: needs refactor
|
||||
- Reads `os.args` directly and calls `print_usage()`/`print_command_help()` as side effects
|
||||
- Cannot test without either accepting `[]string` param or extracting output
|
||||
- Minimal refactor: `parse_args(args: []string)` — caller passes `os.args`, tests pass synthetic slices
|
||||
- Return values (`ok`, `cmd.name`, `cmd.flags`, `cmd.bool_set`) are the interesting part to assert
|
||||
- Test cases: bare command, `--flag value`, `-f value`, positional args, `--help`/`-h`, unknown command, no args, mixed flags + positionals
|
||||
|
||||
### 3. `is_encrypted_key` (ssh.odin)
|
||||
- Test cases: encrypted key (returns true), unencrypted key (returns false), RSA key, malformed key
|
||||
- Fills last gap in ssh.odin
|
||||
|
||||
## Tier 2 — High value, medium effort (fixtures exist)
|
||||
|
||||
### 4. `db.odin` CRUD layer
|
||||
Largest gap in the project. Infrastructure already in `db_integration_test.odin` (`fixture_key`, `fixture_db_path`, in-memory DB setup).
|
||||
|
||||
Procs to test:
|
||||
- `db_open` / `db_close` — open in-memory DB, verify handle valid
|
||||
- `db_insert` — insert a row, verify it persists
|
||||
- `db_fetch` — fetch existing row, fetch missing row (returns false)
|
||||
- `db_delete` — delete existing row (returns true), delete missing row (returns false)
|
||||
- `db_list` — list multiple rows, empty DB
|
||||
- `db_vacuum_to_file` — vacuum to temp file, verify file exists and is non-empty
|
||||
|
||||
Test pattern: create in-memory DB via `db_open`, insert fixture rows, query and assert, `defer db_close`.
|
||||
|
||||
### 5. `load_config` / `save_config` (config.odin)
|
||||
- `save_config`: write a `Config` to temp dir, verify file exists and contents are valid JSON
|
||||
- `load_config`: read back a config written by `save_config`, round-trip assert
|
||||
- `load_config` error case: missing file returns error
|
||||
- Need a temp dir fixture (pattern exists in `scan_test.odin`)
|
||||
|
||||
## Tier 3 — Command handlers (need DB + filesystem fixtures)
|
||||
|
||||
### 6. `cmd_version` (cmd_version.odin)
|
||||
- Test default output (prints VERSION)
|
||||
- Test `--long`/`-l` flag output
|
||||
- Capture stdout, assert content
|
||||
|
||||
### 7. `cmd_list` (cmd_list.odin)
|
||||
- Test TTY path: fixture DB with rows, capture table output
|
||||
- Test non-TTY path: capture JSON output, unmarshal and verify keys/values
|
||||
- Test empty DB: verify clean output (empty table or `[]`)
|
||||
|
||||
### 8. `cmd_backup` (cmd_backup.odin)
|
||||
- Test successful backup: valid path, verify `db_insert` called
|
||||
- Test missing file: verify error message
|
||||
- Test duplicate backup: verify rejection or update behavior
|
||||
|
||||
### 9. `cmd_remove` (cmd_remove.odin)
|
||||
- Test successful removal: existing entry, verify `db_delete` called
|
||||
- Test removal of non-existent entry: verify error or no-op
|
||||
|
||||
### 10. `cmd_restore` (cmd_restore.odin)
|
||||
- Test successful restore: entry exists in DB, verify file written to correct path
|
||||
- Test restore of missing entry: verify error
|
||||
- Test directory creation: restore to path with missing parent dirs
|
||||
|
||||
## Tier 4 — Hard to test (interactive / external deps)
|
||||
|
||||
### 11. `cmd_deps` (cmd_deps.odin)
|
||||
- Needs `git` and/or `fd` in PATH
|
||||
- Test TTY and non-TTY paths
|
||||
- Skip if dependencies not available (with `#assert` like TODO 28 suggests)
|
||||
|
||||
### 12. `cmd_scan` (cmd_scan.odin)
|
||||
- Needs `fd` installed
|
||||
- Test with fixture git repo containing `.env` files
|
||||
- Test `find_unbacked` integration (already partially tested in `cmd_check_test.odin`)
|
||||
- Non-TTY JSON output path
|
||||
|
||||
### 13. `cmd_edit_config` (cmd_edit_config.odin)
|
||||
- Needs refactoring: extract `$EDITOR` parsing into testable helper (TODO 12)
|
||||
- Test multi-word editor values (`"code -w"`)
|
||||
- Test missing `$EDITOR`
|
||||
|
||||
### 14. `cmd_init` (cmd_init.odin)
|
||||
- Interactive prompt makes this hard
|
||||
- Needs refactoring: extract SSH key discovery and config generation into testable procs
|
||||
- Test `--force` flag behavior
|
||||
|
||||
### 15. `prompt.odin`
|
||||
- Needs refactoring to be testable
|
||||
- `render_options` could be tested if it accepted an `io.Writer`
|
||||
- `read_key` could be tested with a pipe/redirect instead of raw stdin
|
||||
- `multi_select` is end-to-end interactive, likely integration test only
|
||||
|
||||
## Notes
|
||||
|
||||
- All command handler tests will need stdout capture. Consider extracting a helper or using `io.Writer` injection.
|
||||
- DB integration tests should use in-memory SQLite (`:memory:`) where possible.
|
||||
- Temp dir fixtures should follow the pattern in `scan_test.odin`.
|
||||
- External dependency tests (`fd`, `git`) should use `#assert` to ensure the dependency is present rather than silently skipping (TODO 28).
|
||||
34
TODOS.md
34
TODOS.md
@@ -64,41 +64,43 @@ Note: These todos can wait until all the subcommands have been ported.
|
||||
|
||||
38. Try to do all encryption / decryption in memory - only read / write encrypted data to disk.
|
||||
|
||||
40. use a buffered writer where possible (mem.DEFAULT_PAGE_SIZE)
|
||||
|
||||
## Double-check AI output
|
||||
|
||||
- [ ] cli.odin
|
||||
- [ ] config.odin
|
||||
- [ ] crypto.odin
|
||||
- [ ] db.odin
|
||||
- [ ] features.odin
|
||||
- [ ] main.odin
|
||||
- [ ] prompt.odin
|
||||
- [ ] scan.odin
|
||||
- [ ] sodium.odin
|
||||
- [ ] ssh.odin
|
||||
- [ ] table.odin
|
||||
- [ ] cli_test.odin
|
||||
- [ ] cmd_backup.odin
|
||||
- [ ] cmd_check.odin
|
||||
- [ ] cmd_check_test.odin
|
||||
- [ ] cmd_deps.odin
|
||||
- [ ] cmd_edit_config.odin
|
||||
- [ ] cmd_init.odin
|
||||
- [ ] cmd_list.odin
|
||||
- [ ] cmd_list_test.odin
|
||||
- [ ] cmd_nushell_completion.odin
|
||||
- [ ] cmd_nushell_completion_test.odin
|
||||
- [ ] cmd_remove.odin
|
||||
- [ ] cmd_restore.odin
|
||||
- [ ] cmd_scan.odin
|
||||
- [ ] cmd_sync.odin
|
||||
- [ ] cmd_version.odin
|
||||
- [ ] sqlite/sqlite.odin
|
||||
- [ ] cli_test.odin
|
||||
- [ ] cmd_check_test.odin
|
||||
- [ ] cmd_list_test.odin
|
||||
- [ ] cmd_nushell_completion_test.odin
|
||||
- [x] cmd_version.odin
|
||||
- [ ] config.odin
|
||||
- [ ] config_test.odin
|
||||
- [ ] crypto.odin
|
||||
- [ ] crypto_test.odin
|
||||
- [ ] db.odin
|
||||
- [ ] db_integration_test.odin
|
||||
- [ ] db_test.odin
|
||||
- [ ] features.odin
|
||||
- [ ] features_test.odin
|
||||
- [ ] main.odin
|
||||
- [ ] prompt.odin
|
||||
- [ ] scan.odin
|
||||
- [ ] scan_test.odin
|
||||
- [ ] sodium.odin
|
||||
- [ ] sqlite/sqlite.odin
|
||||
- [ ] ssh.odin
|
||||
- [ ] ssh_test.odin
|
||||
- [ ] table.odin
|
||||
- [ ] table_test.odin
|
||||
|
||||
15
cli.odin
15
cli.odin
@@ -46,23 +46,24 @@ COMMANDS := []CommandInfo {
|
||||
},
|
||||
{"version", "envr version", "Show envr's version", "", {}},
|
||||
{"edit-config", "envr edit-config", "Edit your config with your default editor", "", {}},
|
||||
{"nushell-completion", "envr nushell-completion", "Generate custom completions for nushell", "", {}},
|
||||
{
|
||||
"nushell-completion",
|
||||
"envr nushell-completion",
|
||||
"Generate custom completions for nushell",
|
||||
"",
|
||||
{},
|
||||
},
|
||||
}
|
||||
|
||||
parse_args :: proc() -> (cmd: Command, ok: bool) {
|
||||
args := os.args
|
||||
if len(args) < 2 {
|
||||
if len(args) < 2 || args[1] == "--help" || args[1] == "-h" {
|
||||
print_usage()
|
||||
return Command{}, false
|
||||
}
|
||||
|
||||
cmd.name = args[1]
|
||||
|
||||
if cmd.name == "--help" || cmd.name == "-h" {
|
||||
print_usage()
|
||||
return Command{}, false
|
||||
}
|
||||
|
||||
cmd.args = make([dynamic]string)
|
||||
cmd.flags = make(map[string]string)
|
||||
cmd.bool_set = make(map[string]bool)
|
||||
|
||||
@@ -17,18 +17,22 @@ cmd_backup :: proc(cmd: ^Command) {
|
||||
|
||||
file, ok := new_env_file(path)
|
||||
if !ok {
|
||||
// TODO: log a message
|
||||
return
|
||||
}
|
||||
|
||||
db, db_ok := db_open()
|
||||
if !db_ok {
|
||||
// TODO: log a message
|
||||
return
|
||||
}
|
||||
defer db_close(&db)
|
||||
|
||||
if !db_insert(&db, file) {
|
||||
// TODO: log a message
|
||||
return
|
||||
}
|
||||
|
||||
fmt.printf("Saved %s into the database\n", path)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ cmd_deps :: proc(cmd: ^Command) {
|
||||
}
|
||||
|
||||
if terminal.is_terminal(os.stdout) {
|
||||
render_table(headers, rows[:])
|
||||
w := io.to_writer(os.to_writer(os.stdout))
|
||||
render_table(w, headers, rows[:])
|
||||
} else {
|
||||
w := io.to_writer(os.to_writer(os.stdout))
|
||||
render_json_rows(w, headers, rows[:])
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:io"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
import "core:strings"
|
||||
@@ -38,7 +39,8 @@ cmd_list :: proc(cmd: ^Command) {
|
||||
append(&table_rows, row_slice)
|
||||
}
|
||||
|
||||
render_table(headers, table_rows[:])
|
||||
w := io.to_writer(os.to_writer(os.stdout))
|
||||
render_table(w, headers, table_rows[:])
|
||||
} else {
|
||||
entries: [dynamic]ListEntry
|
||||
for row in rows {
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:io"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
import "core:terminal"
|
||||
@@ -80,7 +81,8 @@ cmd_sync :: proc(cmd: ^Command) {
|
||||
append(&table_rows, row_slice)
|
||||
}
|
||||
|
||||
render_table(headers, table_rows[:])
|
||||
w := io.to_writer(os.to_writer(os.stdout))
|
||||
render_table(w, headers, table_rows[:])
|
||||
} else {
|
||||
data, marshal_err := json.marshal(results[:])
|
||||
if marshal_err != nil {
|
||||
|
||||
@@ -5,10 +5,6 @@ import "core:fmt"
|
||||
VERSION :: #load("version.txt", string)
|
||||
|
||||
cmd_version :: proc(cmd: ^Command) {
|
||||
if has_flag(cmd, "long") || has_flag(cmd, "l") {
|
||||
fmt.printf("envr version %s\n", VERSION)
|
||||
} else {
|
||||
fmt.println(VERSION)
|
||||
}
|
||||
fmt.println(VERSION)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package main
|
||||
import "core:fmt"
|
||||
import "core:testing"
|
||||
|
||||
CRYPTO_TEST_KEY_DIR :: "/tmp/envr-test-keys"
|
||||
CRYPTO_TEST_KEY_DIR :: "fixtures/keys"
|
||||
|
||||
make_test_key_pair :: proc(name: string) -> SshKeyPair {
|
||||
priv := fmt.tprintf("%s/%s", CRYPTO_TEST_KEY_DIR, name)
|
||||
|
||||
@@ -8,16 +8,22 @@ import "core:testing"
|
||||
|
||||
import "sqlite"
|
||||
|
||||
FIXTURES :: "/home/spencer/github.com/envr-zig/fixtures"
|
||||
FIXTURES :: "fixtures"
|
||||
|
||||
fixture_key :: proc() -> SshKeyPair {
|
||||
priv, _ := strings.concatenate([]string{FIXTURES, "/insecure-test-key"}, context.allocator)
|
||||
pub, _ := strings.concatenate([]string{FIXTURES, "/insecure-test-key.pub"}, context.allocator)
|
||||
priv, _ := strings.concatenate(
|
||||
[]string{FIXTURES, "/keys/insecure-test-key"},
|
||||
context.temp_allocator,
|
||||
)
|
||||
pub, _ := strings.concatenate(
|
||||
[]string{FIXTURES, "/keys/insecure-test-key.pub"},
|
||||
context.temp_allocator,
|
||||
)
|
||||
return SshKeyPair{Private = priv, Public = pub}
|
||||
}
|
||||
|
||||
fixture_db_path :: proc() -> string {
|
||||
p, _ := strings.concatenate([]string{FIXTURES, "/single-file.db"}, context.allocator)
|
||||
p, _ := strings.concatenate([]string{FIXTURES, "/single-file.db"}, context.temp_allocator)
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
232
db_test.odin
232
db_test.odin
@@ -1,7 +1,239 @@
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:testing"
|
||||
|
||||
import "sqlite"
|
||||
|
||||
make_test_db :: proc() -> (Db, bool) {
|
||||
db: ^rawptr
|
||||
rc := sqlite.db_open(":memory:", &db)
|
||||
if rc != sqlite.OK {
|
||||
return Db{}, false
|
||||
}
|
||||
|
||||
create_sql := "CREATE TABLE IF NOT EXISTS envr_env_files (path TEXT PRIMARY KEY NOT NULL, remotes TEXT, sha256 TEXT NOT NULL, contents TEXT NOT NULL)"
|
||||
rc = sqlite.db_exec(db, string_to_cstring(create_sql), nil, nil, nil)
|
||||
if rc != sqlite.OK {
|
||||
sqlite.db_close(db)
|
||||
return Db{}, false
|
||||
}
|
||||
|
||||
return Db{db = db}, true
|
||||
}
|
||||
|
||||
make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {}) -> EnvFile {
|
||||
f := EnvFile {
|
||||
Path = path,
|
||||
Dir = "",
|
||||
Sha256 = sha,
|
||||
contents = contents,
|
||||
Remotes = make([dynamic]string, 0, len(remotes)),
|
||||
}
|
||||
for r in remotes {
|
||||
append(&f.Remotes, r)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_insert_and_fetch :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
f := make_test_env_file(
|
||||
"/project/.env",
|
||||
"abc123",
|
||||
"SECRET=value",
|
||||
[]string{"git@github.com:user/repo.git"},
|
||||
)
|
||||
defer delete(f.Remotes)
|
||||
|
||||
testing.expect(t, db_insert(&d, f), "insert should succeed")
|
||||
|
||||
fetched, fetch_ok := db_fetch(&d, "/project/.env")
|
||||
testing.expect(t, fetch_ok, "fetch should succeed")
|
||||
if !fetch_ok do return
|
||||
defer delete(fetched.Remotes)
|
||||
|
||||
testing.expect(t, fetched.Path == "/project/.env", "path mismatch")
|
||||
testing.expect(t, fetched.Sha256 == "abc123", "sha mismatch")
|
||||
testing.expect(t, fetched.contents == "SECRET=value", "contents mismatch")
|
||||
testing.expect(t, len(fetched.Remotes) == 1, "remotes count mismatch")
|
||||
testing.expect(t, fetched.Remotes[0] == "git@github.com:user/repo.git", "remote mismatch")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_fetch_missing :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
_, fetch_ok := db_fetch(&d, "/nonexistent/.env")
|
||||
testing.expect(t, !fetch_ok, "fetch missing should return false")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_insert_or_replace :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
f1 := make_test_env_file("/project/.env", "sha1", "KEY=old")
|
||||
defer delete(f1.Remotes)
|
||||
testing.expect(t, db_insert(&d, f1), "first insert should succeed")
|
||||
|
||||
f2 := make_test_env_file("/project/.env", "sha2", "KEY=new")
|
||||
defer delete(f2.Remotes)
|
||||
testing.expect(t, db_insert(&d, f2), "second insert should succeed")
|
||||
|
||||
results, list_ok := db_list(&d)
|
||||
testing.expect(t, list_ok, "list should succeed")
|
||||
if !list_ok do return
|
||||
defer delete(results)
|
||||
|
||||
testing.expect(t, len(results) == 1, "should have 1 row, not 2")
|
||||
|
||||
fetched, fetch_ok := db_fetch(&d, "/project/.env")
|
||||
testing.expect(t, fetch_ok, "fetch should succeed")
|
||||
if !fetch_ok do return
|
||||
defer delete(fetched.Remotes)
|
||||
|
||||
testing.expect(t, fetched.contents == "KEY=new", "contents should be updated")
|
||||
testing.expect(t, fetched.Sha256 == "sha2", "sha should be updated")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_delete_existing :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
||||
defer delete(f.Remotes)
|
||||
db_insert(&d, f)
|
||||
|
||||
testing.expect(t, db_delete(&d, "/project/.env"), "delete should return true")
|
||||
|
||||
_, fetch_ok := db_fetch(&d, "/project/.env")
|
||||
testing.expect(t, !fetch_ok, "row should be gone after delete")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_delete_missing :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
testing.expect(t, !db_delete(&d, "/nonexistent/.env"), "delete missing should return false")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_list_multiple :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
f1 := make_test_env_file("/proj1/.env", "sha1", "A=1", []string{"git@github.com:a/repo.git"})
|
||||
f2 := make_test_env_file("/proj2/.env", "sha2", "B=2", []string{"git@github.com:b/repo.git"})
|
||||
f3 := make_test_env_file("/proj3/.env", "sha3", "C=3")
|
||||
defer delete(f1.Remotes)
|
||||
defer delete(f2.Remotes)
|
||||
defer delete(f3.Remotes)
|
||||
|
||||
db_insert(&d, f1)
|
||||
db_insert(&d, f2)
|
||||
db_insert(&d, f3)
|
||||
|
||||
results, list_ok := db_list(&d)
|
||||
testing.expect(t, list_ok, "list should succeed")
|
||||
if !list_ok do return
|
||||
defer delete(results)
|
||||
|
||||
testing.expect(t, len(results) == 3, "should have 3 rows")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_list_empty :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
results, list_ok := db_list(&d)
|
||||
testing.expect(t, list_ok, "list should succeed on empty db")
|
||||
testing.expect(t, len(results) == 0, "should have 0 rows")
|
||||
if list_ok do delete(results)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_insert_sets_changed :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
testing.expect(t, !d.changed, "changed should start false")
|
||||
|
||||
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
||||
defer delete(f.Remotes)
|
||||
db_insert(&d, f)
|
||||
|
||||
testing.expect(t, d.changed, "changed should be true after insert")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_delete_sets_changed :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
||||
defer delete(f.Remotes)
|
||||
db_insert(&d, f)
|
||||
d.changed = false
|
||||
|
||||
db_delete(&d, "/project/.env")
|
||||
testing.expect(t, d.changed, "changed should be true after delete")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_vacuum_to_file :: proc(t: ^testing.T) {
|
||||
d, ok := make_test_db()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
if !ok do return
|
||||
defer sqlite.db_close(d.db)
|
||||
|
||||
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
||||
defer delete(f.Remotes)
|
||||
db_insert(&d, f)
|
||||
|
||||
vacuum_path := fmt.tprintf("/tmp/envr-test-vacuum-%d.db", os.get_pid())
|
||||
defer os.remove(vacuum_path)
|
||||
|
||||
testing.expect(t, db_vacuum_to_file(d.db, vacuum_path), "vacuum should succeed")
|
||||
|
||||
_, stat_err := os.stat(vacuum_path, context.allocator)
|
||||
testing.expect(t, stat_err == nil, "vacuumed file should exist")
|
||||
if stat_err != nil do return
|
||||
|
||||
data, read_err := os.read_entire_file_from_path(vacuum_path, context.allocator)
|
||||
testing.expect(t, read_err == nil, "should read vacuumed file")
|
||||
if read_err != nil do return
|
||||
defer delete(data)
|
||||
|
||||
testing.expect(t, len(data) > 0, "vacuumed file should be non-empty")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_update_required_noop :: proc(t: ^testing.T) {
|
||||
testing.expect(t, !db_update_required({}), "Noop should not require update")
|
||||
|
||||
7
fixtures/keys/insecure-test-key
Normal file
7
fixtures/keys/insecure-test-key
Normal file
@@ -0,0 +1,7 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACCbll0MJper9prPwGn2wwikH3hTByL8tlzmhViuvfrryAAAAJCkxfzapMX8
|
||||
2gAAAAtzc2gtZWQyNTUxOQAAACCbll0MJper9prPwGn2wwikH3hTByL8tlzmhViuvfrryA
|
||||
AAAEDXQExhs89b3fjqJHkhuo9QX4JEjXiEC+vSnCAYc8OxcpuWXQwml6v2ms/AafbDCKQf
|
||||
eFMHIvy2XOaFWK69+uvIAAAACnNwZW5jZXJAZncBAgM=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
fixtures/keys/insecure-test-key.pub
Normal file
1
fixtures/keys/insecure-test-key.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJuWXQwml6v2ms/AafbDCKQfeFMHIvy2XOaFWK69+uvI spencer@fw
|
||||
7
fixtures/keys/test_ed25519
Normal file
7
fixtures/keys/test_ed25519
Normal file
@@ -0,0 +1,7 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACC4CdhiPHmU44cyy9UZV1ISnDq9RbYl1m1qTYOXaSNougAAAIg+8A82PvAP
|
||||
NgAAAAtzc2gtZWQyNTUxOQAAACC4CdhiPHmU44cyy9UZV1ISnDq9RbYl1m1qTYOXaSNoug
|
||||
AAAEAalxEoCavixCImtND1I0YHZZjhOrBLxk//t9v0sjYNVLgJ2GI8eZTjhzLL1RlXUhKc
|
||||
Or1FtiXWbWpNg5dpI2i6AAAABHRlc3QB
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
fixtures/keys/test_ed25519.pub
Normal file
1
fixtures/keys/test_ed25519.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILgJ2GI8eZTjhzLL1RlXUhKcOr1FtiXWbWpNg5dpI2i6 test
|
||||
8
fixtures/keys/test_ed25519_encrypted
Normal file
8
fixtures/keys/test_ed25519_encrypted
Normal file
@@ -0,0 +1,8 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABD342Kol/
|
||||
iE3kW3alqJTPVpAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIF29NuS3O0JUKCj4
|
||||
j/NmmJJyJk6n/MwI37WtVeWAC5c/AAAAoPFp0zRQufp8S+f68atSqFT1FYMUvGqL2cmmtJ
|
||||
r+kXEeEvSGdi3xAxCSLuoe0tMeUYP8aUP1M5L9VzTpFoi8jBIfcPl/ZRX8F/+J4dhp5jno
|
||||
3nQuo1AN0D60r+UmmX+Z0IzIrD2jIpZ/Y7P2kXT8OErIhtC4ZJs3nIIOKFY7ZzlM1IqbYH
|
||||
dSSlpUnsAoMPjMb0eD0Q6s6JaldfiNshckauU=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
fixtures/keys/test_ed25519_encrypted.pub
Normal file
1
fixtures/keys/test_ed25519_encrypted.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF29NuS3O0JUKCj4j/NmmJJyJk6n/MwI37WtVeWAC5c/ encrypted test key
|
||||
7
fixtures/keys/test_ed25519_second
Normal file
7
fixtures/keys/test_ed25519_second
Normal file
@@ -0,0 +1,7 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACCZhSOlxHj1zxd+P7adxHOjo3tqqe68AVQ1itJ96nJ95wAAAIh6gz6PeoM+
|
||||
jwAAAAtzc2gtZWQyNTUxOQAAACCZhSOlxHj1zxd+P7adxHOjo3tqqe68AVQ1itJ96nJ95w
|
||||
AAAEAEsVzs6egkWMZolD/pZCX5ZcZVXfd5wZ6Ja12f+PxAQJmFI6XEePXPF34/tp3Ec6Oj
|
||||
e2qp7rwBVDWK0n3qcn3nAAAABXRlc3Qy
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
fixtures/keys/test_ed25519_second.pub
Normal file
1
fixtures/keys/test_ed25519_second.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJmFI6XEePXPF34/tp3Ec6Oje2qp7rwBVDWK0n3qcn3n test2
|
||||
27
fixtures/keys/test_rsa
Normal file
27
fixtures/keys/test_rsa
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEAjwq/ISeK/TmKiV1NABIq+tFwevArpTRTyZ9eC5JyGvDzDB03buVl
|
||||
6bXd6+cwv+h0AZa7BZN60ayv8zAUmyGpSxFN2gMFiJ/0iFYpTHiLZD4VUH8mCPllIehOdr
|
||||
epchmlh14BeShJjlGzwBAlgiEON5V62gCWWLmkIzcAgUd3R2NUQfajl74wA0JBkaNeFwUp
|
||||
nUARyPUeMVX8ZVUvbpE/WOFTZYfFZDkul6aSkAzEeyZq9s4qJ2mWt5acuXcMcUl6YtuAGM
|
||||
Xii+uV1nJyQpNgHRdEZ2Ch1zmtiTrqjutdBUOfyQZJ3Ln9h/nPJDerUHZboyhu654dLbac
|
||||
0P3pYciW8wAAA8BvZFJ5b2RSeQAAAAdzc2gtcnNhAAABAQCPCr8hJ4r9OYqJXU0AEir60X
|
||||
B68CulNFPJn14LknIa8PMMHTdu5WXptd3r5zC/6HQBlrsFk3rRrK/zMBSbIalLEU3aAwWI
|
||||
n/SIVilMeItkPhVQfyYI+WUh6E52t6lyGaWHXgF5KEmOUbPAECWCIQ43lXraAJZYuaQjNw
|
||||
CBR3dHY1RB9qOXvjADQkGRo14XBSmdQBHI9R4xVfxlVS9ukT9Y4VNlh8VkOS6XppKQDMR7
|
||||
Jmr2zionaZa3lpy5dwxxSXpi24AYxeKL65XWcnJCk2AdF0RnYKHXOa2JOuqO610FQ5/JBk
|
||||
ncuf2H+c8kN6tQdlujKG7rnh0ttpzQ/elhyJbzAAAAAwEAAQAAAQAVAR96x1s1/vaUYDJ3
|
||||
4bMU/J83NkA6dJofH7tIGLuPsDUIYNvseVwDOxT42IyEiaZLO26ADZ1535FAtR05gHJjFw
|
||||
nnCw2Ld+2I/Zn35DWXxTQNC3ay16hdl8a50RNdMV3oqEmwGFXgw6eQ+u3/E0qKp/UPwQlS
|
||||
wwPStfdphGyD+15BxNcc/ZTAByKe9JMi7KkygE02jUn9OMPjJJT9RR+oRXZHLq+yU8Fayl
|
||||
QUDgmU5Vq8Mhp0P4JrmCMVeZuRhMPrk3XaDJFPgfSMY1fKEapW6itwsG9VTh6xUMxks26t
|
||||
hk/GuGNjhmt5NOKpQDLLOTKd22u+PZ6kJJQcJjsj47ktAAAAgGcWjHLNm6T0Dp1p5hgfPy
|
||||
QK019Xp24V1zlejyC0iykzBaC+ZFFS9JOBkqfdrrEE1nAzLvJblhUeWpmLBaqOF+PpPxkF
|
||||
oAGXzYck2axVcXhpvgB71uOARGZntVDoxVoOC7vT6I2h8eL75pZNGYJZt1K9Zufr4UwNR4
|
||||
F+FY194pSLAAAAgQDEx1MSFuVZ5sfAH7RteSHWjvyD/CWwbhVzL3IWeUXCMsf9HwUZZd8e
|
||||
zgyqE6Dh65GTXviuy8Tpb4gT4Gne/QblMHGvdbFMlXNOfzz9U5VkF0q1Y/D4rN0Sa7+nzR
|
||||
lZx/LKM20egfypNeJWBQT5KzZ8gEOamL7Qyyk5YG2q5evWnwAAAIEAuhdRyPjXaCM2NyvO
|
||||
dPxvbnpEJZDWRw6iVWtzPAXgwIiI6ngEUVXK2O8T8j0Ufssk3AVbVj1OH8/KJonyWUbedM
|
||||
mDaFhs4Uvd9iuSZdpS7PbLqHYonurg3m6dz4TrtoWUQuBATdGuIGrtkN+Y83e6UqOGT7lY
|
||||
Vqw7lPqhNUowAy0AAAAIdGVzdC1yc2EBAgM=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
fixtures/keys/test_rsa.pub
Normal file
1
fixtures/keys/test_rsa.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCPCr8hJ4r9OYqJXU0AEir60XB68CulNFPJn14LknIa8PMMHTdu5WXptd3r5zC/6HQBlrsFk3rRrK/zMBSbIalLEU3aAwWIn/SIVilMeItkPhVQfyYI+WUh6E52t6lyGaWHXgF5KEmOUbPAECWCIQ43lXraAJZYuaQjNwCBR3dHY1RB9qOXvjADQkGRo14XBSmdQBHI9R4xVfxlVS9ukT9Y4VNlh8VkOS6XppKQDMR7Jmr2zionaZa3lpy5dwxxSXpi24AYxeKL65XWcnJCk2AdF0RnYKHXOa2JOuqO610FQ5/JBkncuf2H+c8kN6tQdlujKG7rnh0ttpzQ/elhyJbz test-rsa
|
||||
BIN
fixtures/single-file.db
Normal file
BIN
fixtures/single-file.db
Normal file
Binary file not shown.
@@ -3,7 +3,7 @@ package main
|
||||
import "core:fmt"
|
||||
import "core:testing"
|
||||
|
||||
TEST_KEY_DIR :: "/tmp/envr-test-keys"
|
||||
TEST_KEY_DIR :: "fixtures/keys"
|
||||
|
||||
@(test)
|
||||
test_parse_ed25519_public_key :: proc(t: ^testing.T) {
|
||||
@@ -70,3 +70,39 @@ test_read_wire_string :: proc(t: ^testing.T) {
|
||||
testing.expect(t, s2 == "", "expected empty string")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_is_encrypted_key_encrypted :: proc(t: ^testing.T) {
|
||||
testing.expect(
|
||||
t,
|
||||
is_encrypted_key(TEST_KEY_DIR + "/test_ed25519_encrypted"),
|
||||
"encrypted key should be detected as encrypted",
|
||||
)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_is_encrypted_key_unencrypted :: proc(t: ^testing.T) {
|
||||
testing.expect(
|
||||
t,
|
||||
!is_encrypted_key(TEST_KEY_DIR + "/test_ed25519"),
|
||||
"unencrypted key should not be detected as encrypted",
|
||||
)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_is_encrypted_key_rsa_unencrypted :: proc(t: ^testing.T) {
|
||||
testing.expect(
|
||||
t,
|
||||
!is_encrypted_key(TEST_KEY_DIR + "/test_rsa"),
|
||||
"unencrypted RSA key should not be detected as encrypted",
|
||||
)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_is_encrypted_key_missing_file :: proc(t: ^testing.T) {
|
||||
testing.expect(
|
||||
t,
|
||||
is_encrypted_key(TEST_KEY_DIR + "/nonexistent"),
|
||||
"missing file should be treated as encrypted (fail-safe)",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
22
table.odin
22
table.odin
@@ -5,16 +5,16 @@ import "core:fmt"
|
||||
import "core:io"
|
||||
import "core:strings"
|
||||
|
||||
render_table :: proc(headers: []string, rows: [][]string) {
|
||||
render_table :: proc(w: io.Writer, headers: []string, rows: [][]string) {
|
||||
col_widths := make([dynamic]int, 0, len(headers))
|
||||
for i in 0 ..< len(headers) {
|
||||
append(&col_widths, strings.rune_count(headers[i]))
|
||||
}
|
||||
for r in rows {
|
||||
for i in 0 ..< len(r) {
|
||||
w := strings.rune_count(r[i])
|
||||
if i < len(col_widths) && w > col_widths[i] {
|
||||
col_widths[i] = w
|
||||
rw := strings.rune_count(r[i])
|
||||
if i < len(col_widths) && rw > col_widths[i] {
|
||||
col_widths[i] = rw
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ render_table :: proc(headers: []string, rows: [][]string) {
|
||||
defer strings.builder_destroy(&b)
|
||||
defer delete(col_widths)
|
||||
|
||||
hline :: proc(b: ^strings.Builder, left, mid, right: string, widths: [dynamic]int) {
|
||||
hline :: proc(w: io.Writer, b: ^strings.Builder, left, mid, right: string, widths: [dynamic]int) {
|
||||
strings.write_string(b, left)
|
||||
for i in 0 ..< len(widths) {
|
||||
for _ in 0 ..< widths[i] + 2 {
|
||||
@@ -36,11 +36,11 @@ render_table :: proc(headers: []string, rows: [][]string) {
|
||||
strings.write_string(b, right)
|
||||
}
|
||||
}
|
||||
fmt.println(strings.to_string(b^))
|
||||
fmt.wprintf(w, "%s\n", strings.to_string(b^), flush = false)
|
||||
strings.builder_reset(b)
|
||||
}
|
||||
|
||||
hline(&b, "\u250c", "\u252c", "\u2510", col_widths)
|
||||
hline(w, &b, "\u250c", "\u252c", "\u2510", col_widths)
|
||||
|
||||
cell :: proc(b: ^strings.Builder, s: string, width: int) {
|
||||
extra := len(s) - strings.rune_count(s)
|
||||
@@ -51,21 +51,21 @@ render_table :: proc(headers: []string, rows: [][]string) {
|
||||
for i in 0 ..< len(headers) {
|
||||
cell(&b, headers[i], col_widths[i])
|
||||
}
|
||||
fmt.println(strings.to_string(b))
|
||||
fmt.wprintf(w, "%s\n", strings.to_string(b), flush = false)
|
||||
strings.builder_reset(&b)
|
||||
|
||||
hline(&b, "\u251c", "\u253c", "\u2524", col_widths)
|
||||
hline(w, &b, "\u251c", "\u253c", "\u2524", col_widths)
|
||||
|
||||
for r in rows {
|
||||
strings.write_string(&b, "\u2502")
|
||||
for i in 0 ..< len(r) {
|
||||
cell(&b, r[i], col_widths[i])
|
||||
}
|
||||
fmt.println(strings.to_string(b))
|
||||
fmt.wprintf(w, "%s\n", strings.to_string(b), flush = false)
|
||||
strings.builder_reset(&b)
|
||||
}
|
||||
|
||||
hline(&b, "\u2514", "\u2534", "\u2518", col_widths)
|
||||
hline(w, &b, "\u2514", "\u2534", "\u2518", col_widths)
|
||||
}
|
||||
|
||||
render_json_rows :: proc(w: io.Writer, headers: []string, rows: [][]string) {
|
||||
|
||||
@@ -102,3 +102,60 @@ test_render_json_rows_empty :: proc(t: ^testing.T) {
|
||||
testing.expect(t, len(result) == 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_render_table_normal :: proc(t: ^testing.T) {
|
||||
b: strings.Builder
|
||||
strings.builder_init(&b)
|
||||
defer strings.builder_destroy(&b)
|
||||
|
||||
headers := []string{"Name", "Path"}
|
||||
rows := [][]string{{"foo", "/home/user/.env"}, {"bar", "/home/user/project/.env"}}
|
||||
|
||||
w := strings.to_writer(&b)
|
||||
render_table(w, headers, rows)
|
||||
|
||||
output := strings.to_string(b)
|
||||
|
||||
testing.expect(t, strings.contains(output, "Name"), "header 'Name' missing from output")
|
||||
testing.expect(t, strings.contains(output, "Path"), "header 'Path' missing from output")
|
||||
testing.expect(t, strings.contains(output, "foo"), "cell 'foo' missing from output")
|
||||
testing.expect(t, strings.contains(output, "/home/user/.env"), "cell '/home/user/.env' missing from output")
|
||||
testing.expect(t, strings.contains(output, "bar"), "cell 'bar' missing from output")
|
||||
testing.expect(t, strings.contains(output, "/home/user/project/.env"), "cell '/home/user/project/.env' missing")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_render_table_empty :: proc(t: ^testing.T) {
|
||||
b: strings.Builder
|
||||
strings.builder_init(&b)
|
||||
defer strings.builder_destroy(&b)
|
||||
|
||||
headers := []string{"Name"}
|
||||
rows: [][]string
|
||||
|
||||
w := strings.to_writer(&b)
|
||||
render_table(w, headers, rows)
|
||||
|
||||
output := strings.to_string(b)
|
||||
|
||||
testing.expect(t, strings.contains(output, "Name"), "header 'Name' missing from output")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_render_table_unicode :: proc(t: ^testing.T) {
|
||||
b: strings.Builder
|
||||
strings.builder_init(&b)
|
||||
defer strings.builder_destroy(&b)
|
||||
|
||||
headers := []string{"Status", "Detail"}
|
||||
rows := [][]string{{"\u2713 Available", "ok"}, {"\u2717 Missing", "fail"}}
|
||||
|
||||
w := strings.to_writer(&b)
|
||||
render_table(w, headers, rows)
|
||||
|
||||
output := strings.to_string(b)
|
||||
|
||||
testing.expect(t, strings.contains(output, "Available"), "unicode cell content missing")
|
||||
testing.expect(t, strings.contains(output, "Missing"), "unicode cell content missing")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user