/* Copyright 2025 Dnote Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package main import ( "bytes" "fmt" "net/http" "os" "os/exec" "strings" "testing" "time" "github.com/dnote/dnote/pkg/assert" "gorm.io/driver/sqlite" "gorm.io/gorm" ) var testServerBinary string func init() { // Build server binary in temp directory tmpDir := os.TempDir() testServerBinary = fmt.Sprintf("%s/dnote-test-server", tmpDir) buildCmd := exec.Command("go", "build", "-tags", "fts5", "-o", testServerBinary, "../server") if out, err := buildCmd.CombinedOutput(); err != nil { panic(fmt.Sprintf("failed to build server: %v\n%s", err, out)) } } func TestServerStart(t *testing.T) { tmpDB := t.TempDir() + "/test.db" port := "13456" // Use different port to avoid conflicts with main test server // Start server in background cmd := exec.Command(testServerBinary, "start", "--port", port) cmd.Env = append(os.Environ(), "DBPath="+tmpDB, ) if err := cmd.Start(); err != nil { t.Fatalf("failed to start server: %v", err) } // Ensure cleanup cleanup := func() { if cmd.Process != nil { cmd.Process.Kill() cmd.Wait() // Wait for process to fully exit } } defer cleanup() // Wait for server to start and migrations to run time.Sleep(3 * time.Second) // Verify server responds to health check resp, err := http.Get(fmt.Sprintf("http://localhost:%s/health", port)) if err != nil { t.Fatalf("failed to reach server health endpoint: %v", err) } defer resp.Body.Close() assert.Equal(t, resp.StatusCode, 200, "health endpoint should return 200") // Kill server before checking database to avoid locks cleanup() // Verify database file was created if _, err := os.Stat(tmpDB); os.IsNotExist(err) { t.Fatalf("database file was not created at %s", tmpDB) } // Verify migrations ran by checking database db, err := gorm.Open(sqlite.Open(tmpDB), &gorm.Config{}) if err != nil { t.Fatalf("failed to open test database: %v", err) } // Verify migrations ran var count int64 if err := db.Raw("SELECT COUNT(*) FROM schema_migrations").Scan(&count).Error; err != nil { t.Fatalf("schema_migrations table not found: %v", err) } if count == 0 { t.Fatal("no migrations were run") } // Verify FTS table exists and is functional if err := db.Exec("SELECT * FROM notes_fts LIMIT 1").Error; err != nil { t.Fatalf("notes_fts table not found or not functional: %v", err) } } func TestServerVersion(t *testing.T) { cmd := exec.Command("go", "run", "-tags", "fts5", "../server", "version") output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("version command failed: %v", err) } outputStr := string(output) if !strings.Contains(outputStr, "dnote-server-") { t.Errorf("expected version output to contain 'dnote-server-', got: %s", outputStr) } } func TestServerRootCommand(t *testing.T) { cmd := exec.Command(testServerBinary) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("server command failed: %v", err) } outputStr := string(output) assert.Equal(t, strings.Contains(outputStr, "Dnote server - a simple command line notebook"), true, "output should contain description") assert.Equal(t, strings.Contains(outputStr, "start: Start the server"), true, "output should contain start command") assert.Equal(t, strings.Contains(outputStr, "version: Print the version"), true, "output should contain version command") } func TestServerStartHelp(t *testing.T) { cmd := exec.Command(testServerBinary, "start", "--help") output, _ := cmd.CombinedOutput() outputStr := string(output) assert.Equal(t, strings.Contains(outputStr, "dnote-server start [flags]"), true, "output should contain usage") assert.Equal(t, strings.Contains(outputStr, "--port"), true, "output should contain port flag") assert.Equal(t, strings.Contains(outputStr, "--baseUrl"), true, "output should contain baseUrl flag") assert.Equal(t, strings.Contains(outputStr, "--dbPath"), true, "output should contain dbPath flag") assert.Equal(t, strings.Contains(outputStr, "--disableRegistration"), true, "output should contain disableRegistration flag") } func TestServerStartInvalidConfig(t *testing.T) { cmd := exec.Command(testServerBinary, "start") // Set invalid BaseURL to trigger validation failure cmd.Env = []string{"BaseURL=not-a-valid-url"} output, err := cmd.CombinedOutput() // Should exit with non-zero status if err == nil { t.Fatal("expected command to fail with invalid config") } outputStr := string(output) assert.Equal(t, strings.Contains(outputStr, "Error:"), true, "output should contain error message") assert.Equal(t, strings.Contains(outputStr, "Invalid BaseURL"), true, "output should mention invalid BaseURL") assert.Equal(t, strings.Contains(outputStr, "dnote-server start [flags]"), true, "output should show usage") assert.Equal(t, strings.Contains(outputStr, "--baseUrl"), true, "output should show flags") } func TestServerUnknownCommand(t *testing.T) { cmd := exec.Command(testServerBinary, "unknown") output, err := cmd.CombinedOutput() // Should exit with non-zero status if err == nil { t.Fatal("expected command to fail with unknown command") } outputStr := string(output) assert.Equal(t, strings.Contains(outputStr, "Unknown command"), true, "output should contain unknown command message") assert.Equal(t, strings.Contains(outputStr, "Dnote server - a simple command line notebook"), true, "output should show help") } func TestServerUserCreate(t *testing.T) { tmpDB := t.TempDir() + "/test.db" cmd := exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "test@example.com", "--password", "password123") output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("user create failed: %v\nOutput: %s", err, output) } outputStr := string(output) assert.Equal(t, strings.Contains(outputStr, "User created successfully"), true, "output should show success message") assert.Equal(t, strings.Contains(outputStr, "test@example.com"), true, "output should show email") // Verify user exists in database db, err := gorm.Open(sqlite.Open(tmpDB), &gorm.Config{}) if err != nil { t.Fatalf("failed to open database: %v", err) } defer func() { sqlDB, _ := db.DB() sqlDB.Close() }() var count int64 db.Table("users").Count(&count) assert.Equal(t, count, int64(1), "should have created 1 user") } func TestServerUserCreateShortPassword(t *testing.T) { tmpDB := t.TempDir() + "/test.db" cmd := exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "test@example.com", "--password", "short") output, err := cmd.CombinedOutput() // Should fail with short password if err == nil { t.Fatal("expected command to fail with short password") } outputStr := string(output) assert.Equal(t, strings.Contains(outputStr, "password should be longer than 8 characters"), true, "output should show password error") } func TestServerUserResetPassword(t *testing.T) { tmpDB := t.TempDir() + "/test.db" // Create user first createCmd := exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "test@example.com", "--password", "oldpassword123") if output, err := createCmd.CombinedOutput(); err != nil { t.Fatalf("failed to create user: %v\nOutput: %s", err, output) } // Reset password resetCmd := exec.Command(testServerBinary, "user", "reset-password", "--dbPath", tmpDB, "--email", "test@example.com", "--password", "newpassword123") output, err := resetCmd.CombinedOutput() if err != nil { t.Fatalf("reset-password failed: %v\nOutput: %s", err, output) } outputStr := string(output) assert.Equal(t, strings.Contains(outputStr, "Password reset successfully"), true, "output should show success message") } func TestServerUserRemove(t *testing.T) { tmpDB := t.TempDir() + "/test.db" // Create user first createCmd := exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "test@example.com", "--password", "password123") if output, err := createCmd.CombinedOutput(); err != nil { t.Fatalf("failed to create user: %v\nOutput: %s", err, output) } // Remove user with confirmation removeCmd := exec.Command(testServerBinary, "user", "remove", "--dbPath", tmpDB, "--email", "test@example.com") // Pipe "y" to stdin to confirm removal stdin, err := removeCmd.StdinPipe() if err != nil { t.Fatalf("failed to create stdin pipe: %v", err) } // Capture output stdout, err := removeCmd.StdoutPipe() if err != nil { t.Fatalf("failed to create stdout pipe: %v", err) } var stderr bytes.Buffer removeCmd.Stderr = &stderr // Start command if err := removeCmd.Start(); err != nil { t.Fatalf("failed to start remove command: %v", err) } // Wait for prompt and send "y" to confirm if err := assert.RespondToPrompt(stdout, stdin, "Remove user test@example.com?", "y\n", 10*time.Second); err != nil { t.Fatalf("failed to confirm removal: %v", err) } // Wait for command to finish if err := removeCmd.Wait(); err != nil { t.Fatalf("user remove failed: %v\nStderr: %s", err, stderr.String()) } // Verify user was removed db, err := gorm.Open(sqlite.Open(tmpDB), &gorm.Config{}) if err != nil { t.Fatalf("failed to open database: %v", err) } defer func() { sqlDB, _ := db.DB() sqlDB.Close() }() var count int64 db.Table("users").Count(&count) assert.Equal(t, count, int64(0), "should have 0 users after removal") } func TestServerUserCreateHelp(t *testing.T) { cmd := exec.Command(testServerBinary, "user", "create", "--help") output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("help command failed: %v\nOutput: %s", err, output) } outputStr := string(output) // Verify help shows double-dash flags for consistency with CLI assert.Equal(t, strings.Contains(outputStr, "--email"), true, "help should show --email (double dash)") assert.Equal(t, strings.Contains(outputStr, "--password"), true, "help should show --password (double dash)") assert.Equal(t, strings.Contains(outputStr, "--dbPath"), true, "help should show --dbPath (double dash)") }