package handlers import ( "context" "encoding/json" "log/slog" "net/http" "os" "testing" "git.nakama.town/fmartingr/hako/internal/archival/archiver" archivalDomain "git.nakama.town/fmartingr/hako/internal/archival/domain" archivalStore "git.nakama.town/fmartingr/hako/internal/archival/store" authDomain "git.nakama.town/fmartingr/hako/internal/auth/domain" "git.nakama.town/fmartingr/hako/internal/database" "git.nakama.town/fmartingr/hako/internal/extractors" "git.nakama.town/fmartingr/hako/internal/model" "git.nakama.town/fmartingr/hako/internal/server/middleware" "git.nakama.town/fmartingr/hako/internal/testutil" "github.com/stretchr/testify/require" ) func TestHandleSystem_NotLoggedIn(t *testing.T) { // Create temporary database for extractor config store tmpDB, err := os.CreateTemp("", "hako_test_*.db") require.NoError(t, err) defer func() { _ = tmpDB.Close() _ = os.Remove(tmpDB.Name()) _ = os.Remove(tmpDB.Name() + "-wal") _ = os.Remove(tmpDB.Name() + "-shm") }() dbURL := "sqlite:" + tmpDB.Name() dbConnections, err := database.NewConnections(dbURL, dbURL) require.NoError(t, err) defer func() { _ = dbConnections.Close() }() // Run migrations err = database.InitSchema(dbConnections) require.NoError(t, err) // Create extractor manager logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) extractorMgr := extractors.NewManager(logger) // Create archiver manager archiverMgr := archiver.NewManager() // Create archiver config store archiverConfigStore := archivalStore.NewArchiverConfigStore(dbConnections.Read, dbConnections.Write) // Create settings store settingsStore := archivalStore.NewSettingsStore(dbConnections.Read, dbConnections.Write) // Create system handler systemHandler := NewSystemHandler(func(ctx context.Context) (string, error) { return model.BuildVersion, nil }, extractorMgr, archiverMgr, archiverConfigStore, settingsStore) // Perform request without authentication w := testutil.PerformRequest(systemHandler.HandleSystem, http.MethodGet, "/system") // Assert response resp := testutil.NewTestResponse(w) resp.AssertStatus(t, http.StatusOK) // Check JSON structure contains version, commit, and date var jsonData map[string]any err = json.Unmarshal(resp.GetBody(), &jsonData) require.NoError(t, err, "Response should be valid JSON") // Check that version, commit, and date keys exist versionValue, ok := jsonData["version"] require.True(t, ok, "Response should contain 'version' key") require.NotEmpty(t, versionValue, "Version should not be empty") commitValue, ok := jsonData["commit"] require.True(t, ok, "Response should contain 'commit' key") dateValue, ok := jsonData["date"] require.True(t, ok, "Response should contain 'date' key") // Verify it's the hako application version (defaults to "dev" in tests) require.Equal(t, model.BuildVersion, versionValue) require.Equal(t, model.BuildCommit, commitValue) require.Equal(t, model.BuildDate, dateValue) } func TestHandleSystem_LoggedIn(t *testing.T) { ctx := context.Background() deps := testutil.GetTestConfigurationAndDependencies(t, ctx) // Initialize domains deps.Dependencies.Domains().SetLinks(archivalDomain.NewLinkDomain(deps.Dependencies)) deps.Dependencies.Domains().SetArchives(archivalDomain.NewArchiveDomain(deps.Dependencies)) deps.Dependencies.Domains().SetCategories(archivalDomain.NewCategoryDomain(deps.Dependencies)) deps.Dependencies.Domains().SetAuth(authDomain.NewAuthDomain(deps.Dependencies)) // Create a test user and get token _, token, err := testutil.NewTestUser(t, deps.Dependencies, "test@example.com", "testpassword123") require.NoError(t, err) // Create extractor manager logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) extractorMgr := extractors.NewManager(logger) // Create archiver manager archiverMgr := archiver.NewManager() // Create archiver config store using database connections from deps archiverConfigStore := archivalStore.NewArchiverConfigStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create settings store settingsStore := archivalStore.NewSettingsStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create system handler systemHandler := NewSystemHandler(func(ctx context.Context) (string, error) { return model.BuildVersion, nil }, extractorMgr, archiverMgr, archiverConfigStore, settingsStore) // Create handler with global auth middleware (sets context if token is present) globalAuthMiddleware := middleware.AuthMiddleware(deps.Dependencies.GetJWTService(), deps.Dependencies.UserStore) handler := globalAuthMiddleware(http.HandlerFunc(systemHandler.HandleSystem)) // Perform authenticated request w := testutil.PerformRequestWithHandler(handler, http.MethodGet, "/system", testutil.WithAuthToken(token), ) // Assert response resp := testutil.NewTestResponse(w) resp.AssertStatus(t, http.StatusOK) // Check JSON structure contains version, commit, and date var jsonData map[string]any err = json.Unmarshal(resp.GetBody(), &jsonData) require.NoError(t, err, "Response should be valid JSON") // Check that version, commit, and date keys exist versionValue, ok := jsonData["version"] require.True(t, ok, "Response should contain 'version' key when logged in") require.NotEmpty(t, versionValue, "Version should not be empty") commitValue, ok := jsonData["commit"] require.True(t, ok, "Response should contain 'commit' key when logged in") dateValue, ok := jsonData["date"] require.True(t, ok, "Response should contain 'date' key when logged in") // Verify it's the hako application version (defaults to "dev" in tests) require.Equal(t, model.BuildVersion, versionValue) require.Equal(t, model.BuildCommit, commitValue) require.Equal(t, model.BuildDate, dateValue) } func TestHandleGetRulesConfig(t *testing.T) { ctx := context.Background() deps := testutil.GetTestConfigurationAndDependencies(t, ctx) // Initialize domains deps.Dependencies.Domains().SetLinks(archivalDomain.NewLinkDomain(deps.Dependencies)) deps.Dependencies.Domains().SetArchives(archivalDomain.NewArchiveDomain(deps.Dependencies)) deps.Dependencies.Domains().SetCategories(archivalDomain.NewCategoryDomain(deps.Dependencies)) deps.Dependencies.Domains().SetAuth(authDomain.NewAuthDomain(deps.Dependencies)) // Create a test user and get token _, token, err := testutil.NewTestUser(t, deps.Dependencies, "test@example.com", "testpassword123") require.NoError(t, err) // Create extractor manager logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) extractorMgr := extractors.NewManager(logger) // Create archiver manager archiverMgr := archiver.NewManager() // Create archiver config store archiverConfigStore := archivalStore.NewArchiverConfigStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create settings store settingsStore := archivalStore.NewSettingsStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create system handler systemHandler := NewSystemHandler(func(ctx context.Context) (string, error) { return model.BuildVersion, nil }, extractorMgr, archiverMgr, archiverConfigStore, settingsStore) // Create handler with global auth middleware globalAuthMiddleware := middleware.AuthMiddleware(deps.Dependencies.GetJWTService(), deps.Dependencies.UserStore) handler := globalAuthMiddleware(http.HandlerFunc(systemHandler.HandleGetRulesConfig)) // Perform authenticated request w := testutil.PerformRequestWithHandler(handler, http.MethodGet, "/system/rules/config", testutil.WithAuthToken(token), ) // Assert response resp := testutil.NewTestResponse(w) resp.AssertStatus(t, http.StatusOK) // Check JSON structure var jsonData map[string]any err = json.Unmarshal(resp.GetBody(), &jsonData) require.NoError(t, err, "Response should be valid JSON") // Check that rules and default_extractors keys exist _, ok := jsonData["rules"] require.True(t, ok, "Response should contain 'rules' key") _, ok = jsonData["default_extractors"] require.True(t, ok, "Response should contain 'default_extractors' key") } func TestHandleSaveRulesConfig(t *testing.T) { ctx := context.Background() deps := testutil.GetTestConfigurationAndDependencies(t, ctx) // Initialize domains deps.Dependencies.Domains().SetLinks(archivalDomain.NewLinkDomain(deps.Dependencies)) deps.Dependencies.Domains().SetArchives(archivalDomain.NewArchiveDomain(deps.Dependencies)) deps.Dependencies.Domains().SetCategories(archivalDomain.NewCategoryDomain(deps.Dependencies)) deps.Dependencies.Domains().SetAuth(authDomain.NewAuthDomain(deps.Dependencies)) // Create a test user and get token _, token, err := testutil.NewTestUser(t, deps.Dependencies, "test@example.com", "testpassword123") require.NoError(t, err) // Create extractor manager logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) extractorMgr := extractors.NewManager(logger) // Create archiver manager archiverMgr := archiver.NewManager() // Create archiver config store archiverConfigStore := archivalStore.NewArchiverConfigStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create settings store settingsStore := archivalStore.NewSettingsStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create system handler systemHandler := NewSystemHandler(func(ctx context.Context) (string, error) { return model.BuildVersion, nil }, extractorMgr, archiverMgr, archiverConfigStore, settingsStore) // Create handler with global auth middleware globalAuthMiddleware := middleware.AuthMiddleware(deps.Dependencies.GetJWTService(), deps.Dependencies.UserStore) handler := globalAuthMiddleware(http.HandlerFunc(systemHandler.HandleSaveRulesConfig)) // Valid rules config validRulesConfig := `{ "rules": [ { "mimetype": "text/html", "extractors": [{"key": "obelisk"}] } ], "default_extractors": [{"key": "obelisk"}, {"key": "thumbnail"}] }` // Perform authenticated request to save rules w := testutil.PerformRequestWithHandler(handler, http.MethodPut, "/system/rules/config", testutil.WithAuthToken(token), testutil.WithBody(validRulesConfig), testutil.WithHeader("Content-Type", "application/json"), ) // Assert response resp := testutil.NewTestResponse(w) resp.AssertStatus(t, http.StatusOK) // Check response message var responseData map[string]string err = json.Unmarshal(resp.GetBody(), &responseData) require.NoError(t, err, "Response should be valid JSON") require.Equal(t, "Rules config saved successfully", responseData["message"]) // Verify rules were saved by getting them back getHandler := globalAuthMiddleware(http.HandlerFunc(systemHandler.HandleGetRulesConfig)) w = testutil.PerformRequestWithHandler(getHandler, http.MethodGet, "/system/rules/config", testutil.WithAuthToken(token), ) resp = testutil.NewTestResponse(w) resp.AssertStatus(t, http.StatusOK) var savedConfig map[string]any err = json.Unmarshal(resp.GetBody(), &savedConfig) require.NoError(t, err) // Verify rules were saved rules, ok := savedConfig["rules"].([]any) require.True(t, ok, "Rules should be an array") require.Len(t, rules, 1, "Should have one rule") } func TestHandleSaveRulesConfig_InvalidJSON(t *testing.T) { ctx := context.Background() deps := testutil.GetTestConfigurationAndDependencies(t, ctx) // Initialize domains deps.Dependencies.Domains().SetLinks(archivalDomain.NewLinkDomain(deps.Dependencies)) deps.Dependencies.Domains().SetArchives(archivalDomain.NewArchiveDomain(deps.Dependencies)) deps.Dependencies.Domains().SetCategories(archivalDomain.NewCategoryDomain(deps.Dependencies)) deps.Dependencies.Domains().SetAuth(authDomain.NewAuthDomain(deps.Dependencies)) // Create a test user and get token _, token, err := testutil.NewTestUser(t, deps.Dependencies, "test@example.com", "testpassword123") require.NoError(t, err) // Create extractor manager logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) extractorMgr := extractors.NewManager(logger) // Create archiver manager archiverMgr := archiver.NewManager() // Create archiver config store archiverConfigStore := archivalStore.NewArchiverConfigStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create settings store settingsStore := archivalStore.NewSettingsStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create system handler systemHandler := NewSystemHandler(func(ctx context.Context) (string, error) { return model.BuildVersion, nil }, extractorMgr, archiverMgr, archiverConfigStore, settingsStore) // Create handler with global auth middleware globalAuthMiddleware := middleware.AuthMiddleware(deps.Dependencies.GetJWTService(), deps.Dependencies.UserStore) handler := globalAuthMiddleware(http.HandlerFunc(systemHandler.HandleSaveRulesConfig)) // Invalid JSON invalidJSON := `{invalid json}` // Perform authenticated request w := testutil.PerformRequestWithHandler(handler, http.MethodPut, "/system/rules/config", testutil.WithAuthToken(token), testutil.WithBody(invalidJSON), testutil.WithHeader("Content-Type", "application/json"), ) // Assert response resp := testutil.NewTestResponse(w) resp.AssertStatus(t, http.StatusBadRequest) } func TestHandleSaveRulesConfig_InvalidRuleStructure(t *testing.T) { ctx := context.Background() deps := testutil.GetTestConfigurationAndDependencies(t, ctx) // Initialize domains deps.Dependencies.Domains().SetLinks(archivalDomain.NewLinkDomain(deps.Dependencies)) deps.Dependencies.Domains().SetArchives(archivalDomain.NewArchiveDomain(deps.Dependencies)) deps.Dependencies.Domains().SetCategories(archivalDomain.NewCategoryDomain(deps.Dependencies)) deps.Dependencies.Domains().SetAuth(authDomain.NewAuthDomain(deps.Dependencies)) // Create a test user and get token _, token, err := testutil.NewTestUser(t, deps.Dependencies, "test@example.com", "testpassword123") require.NoError(t, err) // Create extractor manager logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) extractorMgr := extractors.NewManager(logger) // Create archiver manager archiverMgr := archiver.NewManager() // Create archiver config store archiverConfigStore := archivalStore.NewArchiverConfigStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create settings store settingsStore := archivalStore.NewSettingsStore(deps.DBConnections.Read, deps.DBConnections.Write) // Create system handler systemHandler := NewSystemHandler(func(ctx context.Context) (string, error) { return model.BuildVersion, nil }, extractorMgr, archiverMgr, archiverConfigStore, settingsStore) // Create handler with global auth middleware globalAuthMiddleware := middleware.AuthMiddleware(deps.Dependencies.GetJWTService(), deps.Dependencies.UserStore) handler := globalAuthMiddleware(http.HandlerFunc(systemHandler.HandleSaveRulesConfig)) // Valid JSON but invalid rule structure (missing required fields) invalidRuleConfig := `{ "rules": [ { "mimetype": "" } ], "default_extractors": [] }` // Perform authenticated request w := testutil.PerformRequestWithHandler(handler, http.MethodPut, "/system/rules/config", testutil.WithAuthToken(token), testutil.WithBody(invalidRuleConfig), testutil.WithHeader("Content-Type", "application/json"), ) // Assert response - should fail validation resp := testutil.NewTestResponse(w) resp.AssertStatus(t, http.StatusBadRequest) }