Files
swanadiva 847989df75 refactor: move V1 code into v1/ subdirectory
- git mv cmd/ internal/ pkg/ test/ go.mod go.sum Makefile build.sh docs/ v1/
- Create v1/README.md with V1 documentation
- Update root README for V1 + V2 structure
- V1 still builds (cd v1 && go build ./cmd/hostkeeper) and 105 tests pass
- Root is now clean for V2 development
2026-07-07 11:56:27 +07:00

289 lines
6.7 KiB
Go

package knownhosts_test
import (
"crypto/ed25519"
"crypto/rand"
"os"
"path/filepath"
"testing"
cryptossh "golang.org/x/crypto/ssh"
"git.tukangketik.id/swanadiva/hostkeeper/pkg/knownhosts"
)
func tempDir(t *testing.T) string {
t.Helper()
dir, err := os.MkdirTemp("", "knownhosts-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
return dir
}
func generateTestKey(t *testing.T) cryptossh.PublicKey {
t.Helper()
pubKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
}
sshPubKey, err := cryptossh.NewPublicKey(pubKey)
if err != nil {
t.Fatalf("Failed to create SSH public key: %v", err)
}
return sshPubKey
}
func generateTestKey2(t *testing.T) cryptossh.PublicKey {
t.Helper()
pubKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
}
sshPubKey, err := cryptossh.NewPublicKey(pubKey)
if err != nil {
t.Fatalf("Failed to create SSH public key: %v", err)
}
return sshPubKey
}
// 2.1 New creates file on first call
func TestNewCreatesFile(t *testing.T) {
dir := tempDir(t)
kh, err := knownhosts.New(dir)
if err != nil {
t.Fatalf("New failed: %v", err)
}
if kh == nil {
t.Fatal("New should return non-nil KnownHosts")
}
// File should be created after first Add+Save
}
// 2.2 New loads existing
func TestNewLoadsExisting(t *testing.T) {
dir := tempDir(t)
key := generateTestKey(t)
// Create and add a host
kh1, _ := knownhosts.New(dir)
_ = kh1.Add("example.com", 22, key)
// Load again
kh2, err := knownhosts.New(dir)
if err != nil {
t.Fatalf("New failed: %v", err)
}
stored := kh2.Get("example.com", 22)
if stored == nil {
t.Fatal("Should load existing host from file")
}
}
// 2.3 Add new host
func TestAddNewHost(t *testing.T) {
dir := tempDir(t)
key := generateTestKey(t)
kh, _ := knownhosts.New(dir)
err := kh.Add("example.com", 22, key)
if err != nil {
t.Fatalf("Add failed: %v", err)
}
stored := kh.Get("example.com", 22)
if stored == nil {
t.Fatal("Get should return the added host")
}
if stored.Hostname != "example.com" {
t.Errorf("Hostname = %q, want %q", stored.Hostname, "example.com")
}
if stored.Port != 22 {
t.Errorf("Port = %d, want 22", stored.Port)
}
}
// 2.4 Add duplicate — no error, no duplicate
func TestAddDuplicate(t *testing.T) {
dir := tempDir(t)
key := generateTestKey(t)
kh, _ := knownhosts.New(dir)
_ = kh.Add("example.com", 22, key)
err := kh.Add("example.com", 22, key)
if err != nil {
t.Fatalf("Add duplicate should not error: %v", err)
}
}
// 2.5 Get existing host
func TestGetExisting(t *testing.T) {
dir := tempDir(t)
key := generateTestKey(t)
kh, _ := knownhosts.New(dir)
_ = kh.Add("example.com", 22, key)
stored := kh.Get("example.com", 22)
if stored == nil {
t.Fatal("Get should return existing host")
}
if stored.Hostname != "example.com" {
t.Errorf("Hostname = %q, want %q", stored.Hostname, "example.com")
}
}
// 2.6 Get non-existent host
func TestGetNonExistent(t *testing.T) {
dir := tempDir(t)
kh, _ := knownhosts.New(dir)
stored := kh.Get("unknown.com", 22)
if stored != nil {
t.Error("Get should return nil for non-existent host")
}
}
// 2.7 Remove existing host
func TestRemoveExisting(t *testing.T) {
dir := tempDir(t)
key := generateTestKey(t)
kh, _ := knownhosts.New(dir)
_ = kh.Add("example.com", 22, key)
err := kh.Remove("example.com", 22)
if err != nil {
t.Fatalf("Remove failed: %v", err)
}
stored := kh.Get("example.com", 22)
if stored != nil {
t.Error("Get should return nil after Remove")
}
}
// 2.8 Remove non-existent host — no error
func TestRemoveNonExistent(t *testing.T) {
dir := tempDir(t)
kh, _ := knownhosts.New(dir)
err := kh.Remove("unknown.com", 22)
if err != nil {
t.Fatalf("Remove non-existent should not error: %v", err)
}
}
// 2.9 Verify unknown host — (false, nil) TOFU
func TestVerifyUnknown(t *testing.T) {
dir := tempDir(t)
key := generateTestKey(t)
kh, _ := knownhosts.New(dir)
matches, stored := kh.Verify("unknown.com", 22, key)
if matches {
t.Error("Verify should return false for unknown host")
}
if stored != nil {
t.Error("Verify should return nil HostKey for unknown host")
}
}
// 2.10 Verify known host, matching key — (true, hostKey)
func TestVerifyKnownMatch(t *testing.T) {
dir := tempDir(t)
key := generateTestKey(t)
kh, _ := knownhosts.New(dir)
_ = kh.Add("example.com", 22, key)
matches, stored := kh.Verify("example.com", 22, key)
if !matches {
t.Error("Verify should return true for matching key")
}
if stored == nil {
t.Error("Verify should return stored HostKey")
}
}
// 2.11 Verify known host, mismatched key — (false, hostKey) MITM
func TestVerifyKnownMismatch(t *testing.T) {
dir := tempDir(t)
key1 := generateTestKey(t)
key2 := generateTestKey2(t)
kh, _ := knownhosts.New(dir)
_ = kh.Add("example.com", 22, key1)
matches, stored := kh.Verify("example.com", 22, key2)
if matches {
t.Error("Verify should return false for mismatched key (MITM)")
}
if stored == nil {
t.Error("Verify should return stored HostKey for mismatch")
}
}
// 2.12 HostKeyCallback — autoAdd=true adds unknown hosts
func TestHostKeyCallbackAutoAdd(t *testing.T) {
dir := tempDir(t)
key := generateTestKey(t)
kh, _ := knownhosts.New(dir)
callback := kh.HostKeyCallback(true)
// Simulate host key check via callback
// HostKeyCallback expects net.Addr, so we create a fake one
addr := &fakeAddr{addr: "192.168.1.1:22"}
err := callback("example.com", addr, key)
if err != nil {
t.Fatalf("HostKeyCallback with autoAdd failed: %v", err)
}
// Verify host was added
stored := kh.Get("example.com", 22)
if stored == nil {
t.Error("HostKeyCallback should add unknown host when autoAdd=true")
}
}
// 2.13 Persistence — Add → Save → New → Get
func TestPersistence(t *testing.T) {
dir := tempDir(t)
key := generateTestKey(t)
kh1, _ := knownhosts.New(dir)
_ = kh1.Add("example.com", 22, key)
// Create new instance from same directory
kh2, err := knownhosts.New(dir)
if err != nil {
t.Fatalf("New failed: %v", err)
}
stored := kh2.Get("example.com", 22)
if stored == nil {
t.Fatal("Host should persist across New() calls")
}
}
// 2.14 Corrupted file → error
func TestCorruptedFile(t *testing.T) {
dir := tempDir(t)
path := filepath.Join(dir, "known_hosts")
_ = os.WriteFile(path, []byte("not valid json {{{"), 0600)
_, err := knownhosts.New(dir)
if err == nil {
t.Error("New should return error for corrupted file")
}
}
// fakeAddr implements net.Addr for testing
type fakeAddr struct {
addr string
}
func (f *fakeAddr) Network() string { return "tcp" }
func (f *fakeAddr) String() string { return f.addr }