diff --git a/internal/handlers/notes_test.go b/internal/handlers/notes_test.go index 5dcb0db..2b43398 100644 --- a/internal/handlers/notes_test.go +++ b/internal/handlers/notes_test.go @@ -8,6 +8,9 @@ import ( "runtime" "strings" "testing" + + "github.com/stormlightlabs/noteleaf/internal/models" + "github.com/stormlightlabs/noteleaf/internal/store" ) func setupNoteTest(t *testing.T) (string, func()) { @@ -172,13 +175,490 @@ This is the content of my note.` t.Errorf("Expected no editor error, got: %v", err) } }) + + t.Run("handles database connection error", func(t *testing.T) { + handler.db.Close() + defer func() { + var err error + handler.db, err = store.NewDatabase() + if err != nil { + t.Fatalf("Failed to reconnect to database: %v", err) + } + }() + + err := handler.Edit(ctx, 1) + if err == nil { + t.Error("Edit should fail when database is closed") + } + if !strings.Contains(err.Error(), "failed to get note") { + t.Errorf("Expected database error, got: %v", err) + } + }) + + t.Run("handles temp file creation error", func(t *testing.T) { + testHandler, err := NewNoteHandler() + if err != nil { + t.Fatalf("Failed to create test handler: %v", err) + } + defer testHandler.Close() + + err = testHandler.Create(ctx, "Temp File Test Note", "Test content", "", false) + if err != nil { + t.Fatalf("Failed to create test note: %v", err) + } + + originalTempDir := os.Getenv("TMPDIR") + os.Setenv("TMPDIR", "/non/existent/path") + defer os.Setenv("TMPDIR", originalTempDir) + + err = testHandler.Edit(ctx, 1) + if err == nil { + t.Error("Edit should fail when temp file creation fails") + } + if !strings.Contains(err.Error(), "failed to create temporary file") { + t.Errorf("Expected temp file error, got: %v", err) + } + }) + + t.Run("handles editor failure", func(t *testing.T) { + testHandler, err := NewNoteHandler() + if err != nil { + t.Fatalf("Failed to create test handler: %v", err) + } + defer testHandler.Close() + + err = testHandler.Create(ctx, "Editor Failure Test Note", "Test content", "", false) + if err != nil { + t.Fatalf("Failed to create test note: %v", err) + } + + mockEditor := func(editor, filePath string) error { + return fmt.Errorf("editor process failed") + } + testHandler.openInEditorFunc = mockEditor + + err = testHandler.Edit(ctx, 1) + if err == nil { + t.Error("Edit should fail when editor fails") + } + if !strings.Contains(err.Error(), "failed to open editor") { + t.Errorf("Expected editor error, got: %v", err) + } + }) + + t.Run("handles temp file write error", func(t *testing.T) { + originalHandler := handler.openInEditorFunc + defer func() { handler.openInEditorFunc = originalHandler }() + + mockEditor := func(editor, filePath string) error { + return os.Chmod(filePath, 0444) + } + handler.openInEditorFunc = mockEditor + + err := handler.Edit(ctx, 1) + if err == nil { + t.Error("Edit should handle temp file write issues") + } + }) + + t.Run("handles file read error after editing", func(t *testing.T) { + testHandler, err := NewNoteHandler() + if err != nil { + t.Fatalf("Failed to create test handler: %v", err) + } + defer testHandler.Close() + + err = testHandler.Create(ctx, "File Read Error Test Note", "Test content", "", false) + if err != nil { + t.Fatalf("Failed to create test note: %v", err) + } + + mockEditor := func(editor, filePath string) error { + return os.Remove(filePath) + } + testHandler.openInEditorFunc = mockEditor + + err = testHandler.Edit(ctx, 1) + if err == nil { + t.Error("Edit should fail when temp file is deleted") + } + if !strings.Contains(err.Error(), "failed to read edited content") { + t.Errorf("Expected file read error, got: %v", err) + } + }) + + t.Run("handles database update error", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + id := handler.CreateTestNote(t, "Database Update Error Test Note", "Test content", nil) + + dbHelper := NewDatabaseTestHelper(handler) + dbHelper.DropNotesTable() + + mockEditor := NewMockEditor().WithContent(`# Modified Note + +Modified content here.`) + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, id) + Expect.AssertError(t, err, "failed to get note", "Edit should fail when database is corrupted") + }) + + t.Run("handles validation error - corrupted note content", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + id := handler.CreateTestNote(t, "Corrupted Content Test Note", "Test content", nil) + + invalidContent := string([]byte{0, 1, 2, 255, 254, 253}) + mockEditor := NewMockEditor().WithContent(invalidContent) + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, id) + if err != nil && !strings.Contains(err.Error(), "failed to update note") { + t.Errorf("Edit should handle corrupted content gracefully, got: %v", err) + } + }) + + t.Run("handles validation error - empty note after edit", func(t *testing.T) { + mockEditor := func(editor, filePath string) error { + return os.WriteFile(filePath, []byte(""), 0644) + } + handler.openInEditorFunc = mockEditor + + err := handler.Edit(ctx, 1) + if err != nil { + t.Logf("Edit with empty content handled: %v", err) + } + }) + + t.Run("handles database constraint violations", func(t *testing.T) { + db, dbErr := store.NewDatabase() + if dbErr != nil { + t.Fatalf("Failed to get new database: %v", dbErr) + } + defer db.Close() + + _, execErr := db.Exec(`ALTER TABLE notes ADD CONSTRAINT test_constraint + CHECK (length(title) > 0 AND length(title) < 5)`) + if execErr != nil { + t.Skipf("Could not add constraint for test: %v", execErr) + } + + handler.db.Close() + handler.db = db + + mockEditor := func(editor, filePath string) error { + content := `# This Title Is Way Too Long For The Test Constraint + +Content here.` + return os.WriteFile(filePath, []byte(content), 0644) + } + handler.openInEditorFunc = mockEditor + + err := handler.Edit(ctx, 1) + if err == nil { + t.Error("Edit should fail with constraint violation") + } + if !strings.Contains(err.Error(), "failed to update note") { + t.Errorf("Expected constraint violation error, got: %v", err) + } + }) + + t.Run("handles database transaction rollback", func(t *testing.T) { + handler.db.Close() + var dbErr error + handler.db, dbErr = store.NewDatabase() + if dbErr != nil { + t.Fatalf("Failed to reconnect: %v", dbErr) + } + + handler.db.Exec("BEGIN TRANSACTION") + handler.db.Exec("UPDATE notes SET title = 'locked' WHERE id = 1") + + db2, err2 := store.NewDatabase() + if err2 != nil { + t.Fatalf("Failed to create second connection: %v", err2) + } + defer db2.Close() + + oldDB := handler.db + handler.db = db2 + + mockEditor := func(editor, filePath string) error { + content := `# Modified Title + +Modified content.` + return os.WriteFile(filePath, []byte(content), 0644) + } + handler.openInEditorFunc = mockEditor + + err := handler.Edit(ctx, 1) + + oldDB.Exec("ROLLBACK") + handler.db = oldDB + + if err == nil { + t.Log("Edit succeeded despite transaction conflict") + } + }) + + t.Run("handles successful edit", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + id := handler.CreateTestNote(t, "Edit Test Note", "Original content", nil) + + mockEditor := NewMockEditor().WithContent(`# Modified Edit Test Note + +This is the modified content. + +`) + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, id) + Expect.AssertNoError(t, err, "Edit should succeed") + }) + }) + + t.Run("Edit Errors", func(t *testing.T) { + t.Run("API Failures", func(t *testing.T) { + t.Run("handles non-existent note", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + err := handler.Edit(ctx, 999) + Expect.AssertError(t, err, "failed to get note", "Edit should fail with non-existent note ID") + }) + + t.Run("handles no editor configured", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + envHelper := NewEnvironmentTestHelper() + defer envHelper.RestoreEnv() + + envHelper.UnsetEnv("EDITOR") + envHelper.SetEnv("PATH", "") + + err := handler.Edit(ctx, noteID) + Expect.AssertError(t, err, "failed to open editor", "Edit should fail when no editor is configured") + }) + + t.Run("handles temp file creation error", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + envHelper := NewEnvironmentTestHelper() + defer envHelper.RestoreEnv() + + envHelper.SetEnv("TMPDIR", "/non/existent/path") + + err := handler.Edit(ctx, noteID) + Expect.AssertError(t, err, "failed to create temporary file", "Edit should fail when temp file creation fails") + }) + + t.Run("handles editor failure", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + mockEditor := NewMockEditor().WithFailure("editor process failed") + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + Expect.AssertError(t, err, "failed to open editor", "Edit should fail when editor fails") + }) + + t.Run("handles file read error after editing", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + mockEditor := NewMockEditor().WithFileDeleted() + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + Expect.AssertError(t, err, "failed to read edited content", "Edit should fail when temp file is deleted") + }) + }) + + t.Run("Database Errors", func(t *testing.T) { + t.Run("handles database connection error", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + dbHelper := NewDatabaseTestHelper(handler) + dbHelper.CloseDatabase() + + err := handler.Edit(ctx, noteID) + Expect.AssertError(t, err, "failed to get note", "Edit should fail when database is closed") + }) + + t.Run("handles database update error", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + dbHelper := NewDatabaseTestHelper(handler) + dbHelper.DropNotesTable() + + mockEditor := NewMockEditor().WithContent(`# Modified Note + +Modified content here.`) + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + Expect.AssertError(t, err, "failed to get note", "Edit should fail when database table is missing") + }) + + t.Run("handles database constraint violations", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + _, execErr := handler.db.Exec(`ALTER TABLE notes ADD CONSTRAINT test_constraint + CHECK (length(title) > 0 AND length(title) < 5)`) + if execErr != nil { + t.Skipf("Could not add constraint for test: %v", execErr) + } + + mockEditor := NewMockEditor().WithContent(`# This Title Is Way Too Long For The Test Constraint + +Content here.`) + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + Expect.AssertError(t, err, "failed to update note", "Edit should fail with constraint violation") + }) + }) + + t.Run("Validation Errors", func(t *testing.T) { + t.Run("handles corrupted note content", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + invalidContent := string([]byte{0, 1, 2, 255, 254, 253}) + mockEditor := NewMockEditor().WithContent(invalidContent) + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + if err != nil && !strings.Contains(err.Error(), "failed to update note") { + t.Errorf("Edit should handle corrupted content gracefully, got: %v", err) + } + }) + + t.Run("handles empty note after edit", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + mockEditor := NewMockEditor().WithContent("") + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + if err != nil { + t.Logf("Edit with empty content handled: %v", err) + } + }) + + t.Run("handles very large content", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + largeContent := fmt.Sprintf("# Large Note\n\n%s", strings.Repeat("Large content ", 70000)) + mockEditor := NewMockEditor().WithContent(largeContent) + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + if err != nil { + t.Logf("Edit with large content handled: %v", err) + } else { + t.Log("Edit succeeded with large content") + } + }) + }) + + t.Run("Success Cases", func(t *testing.T) { + t.Run("handles successful edit with title and tags", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Original Note", "Original content", []string{"original"}) + + mockEditor := NewMockEditor().WithContent(`# Modified Note + +This is the modified content. + +`) + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + Expect.AssertNoError(t, err, "Edit should succeed") + + Expect.AssertNoteExists(t, handler, noteID) + }) + + t.Run("handles no changes made", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Test Note", "Test content", nil) + + originalContent := handler.formatNoteForEdit(&models.Note{ + ID: noteID, + Title: "Test Note", + Content: "Test content", + Tags: nil, + }) + mockEditor := NewMockEditor().WithContent(originalContent) + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + Expect.AssertNoError(t, err, "Edit should succeed even with no changes") + }) + + t.Run("handles content without title", func(t *testing.T) { + handler := NewHandlerTestHelper(t) + ctx := context.Background() + + noteID := handler.CreateTestNote(t, "Original Title", "Original content", nil) + + mockEditor := NewMockEditor().WithContent("Just some content without a title") + handler.openInEditorFunc = mockEditor.GetEditorFunc() + + err := handler.Edit(ctx, noteID) + Expect.AssertNoError(t, err, "Edit should succeed without title") + }) + }) }) t.Run("Read/View", func(t *testing.T) { ctx := context.Background() t.Run("views note successfully", func(t *testing.T) { - err := handler.View(ctx, 1) + testHandler, err := NewNoteHandler() + if err != nil { + t.Fatalf("Failed to create test handler: %v", err) + } + defer testHandler.Close() + + err = testHandler.Create(ctx, "View Test Note", "Test content for viewing", "", false) + if err != nil { + t.Fatalf("Failed to create test note: %v", err) + } + + err = testHandler.View(ctx, 1) if err != nil { t.Errorf("View should succeed: %v", err) } @@ -200,14 +680,26 @@ This is the content of my note.` ctx := context.Background() t.Run("lists with archived filter", func(t *testing.T) { - err := handler.List(ctx, true, true, nil) + testHandler, err := NewNoteHandler() + if err != nil { + t.Fatalf("Failed to create test handler: %v", err) + } + defer testHandler.Close() + + err = testHandler.List(ctx, true, true, nil) if err != nil { t.Errorf("List with archived filter should succeed: %v", err) } }) t.Run("lists with tag filter", func(t *testing.T) { - err := handler.List(ctx, true, false, []string{"work", "personal"}) + testHandler, err := NewNoteHandler() + if err != nil { + t.Fatalf("Failed to create test handler: %v", err) + } + defer testHandler.Close() + + err = testHandler.List(ctx, true, false, []string{"work", "personal"}) if err != nil { t.Errorf("List with tag filter should succeed: %v", err) } @@ -234,7 +726,13 @@ This is the content of my note.` ctx := context.Background() t.Run("handles non-existent note", func(t *testing.T) { - err := handler.Delete(ctx, 999) + testHandler, err := NewNoteHandler() + if err != nil { + t.Fatalf("Failed to create test handler: %v", err) + } + defer testHandler.Close() + + err = testHandler.Delete(ctx, 999) if err == nil { t.Error("Delete should fail with non-existent note ID") } @@ -244,42 +742,51 @@ This is the content of my note.` }) t.Run("deletes note successfully", func(t *testing.T) { - err := handler.Create(ctx, "Note to Delete", "This will be deleted", "", false) + testHandler, err := NewNoteHandler() + if err != nil { + t.Fatalf("Failed to create test handler: %v", err) + } + defer testHandler.Close() + + err = testHandler.Create(ctx, "Note to Delete", "This will be deleted", "", false) if err != nil { t.Fatalf("Failed to create test note: %v", err) } - // Delete the note (should be a high ID number since we've created many notes) - err = handler.Delete(ctx, 1) + err = testHandler.Delete(ctx, 1) if err != nil { t.Errorf("Delete should succeed: %v", err) } - err = handler.View(ctx, 1) + err = testHandler.View(ctx, 1) if err == nil { t.Error("Note should be gone after deletion") } }) t.Run("deletes note with file path", func(t *testing.T) { - filePath := createTestMarkdownFile(t, tempDir, "delete-test.md", "# Test Note\n\nTest content") + testTempDir, testCleanup := setupNoteTest(t) + defer testCleanup() - err := handler.Create(ctx, "", "", filePath, false) + testHandler, err := NewNoteHandler() if err != nil { - t.Fatalf("Failed to create test note from file: %v", err) + t.Fatalf("Failed to create test handler: %v", err) } + defer testHandler.Close() + + filePath := createTestMarkdownFile(t, testTempDir, "delete-test.md", "# Test Note\n\nTest content") - err = handler.Create(ctx, "File Note to Delete", "", "", false) + err = testHandler.Create(ctx, "", "", filePath, false) if err != nil { - t.Fatalf("Failed to create file note: %v", err) + t.Fatalf("Failed to create test note from file: %v", err) } - err = handler.Delete(ctx, 2) + err = testHandler.Delete(ctx, 1) if err != nil { t.Errorf("Delete should succeed: %v", err) } - err = handler.View(ctx, 2) + err = testHandler.View(ctx, 1) if err == nil { t.Error("Note should be gone after deletion") } @@ -389,87 +896,55 @@ This is the content of my note.` ctx := context.Background() t.Run("creates note successfully", func(t *testing.T) { - mockEditor := func(editor, filePath string) error { - content := `# Test Interactive Note + handler := NewHandlerTestHelper(t) + mockEditor := NewMockEditor().WithContent(`# Test Interactive Note This is content from the interactive editor. -` - return os.WriteFile(filePath, []byte(content), 0644) - } - - handler.openInEditorFunc = mockEditor +`) + handler.openInEditorFunc = mockEditor.GetEditorFunc() err := handler.createInteractive(ctx) - if err != nil { - t.Errorf("createInteractive should succeed: %v", err) - } + Expect.AssertNoError(t, err, "createInteractive should succeed") }) t.Run("handles cancelled note creation", func(t *testing.T) { - mockEditor := func(editor, filePath string) error { - return nil - } - - handler.openInEditorFunc = mockEditor + handler := NewHandlerTestHelper(t) + mockEditor := NewMockEditor().WithContent("") // Empty content simulates cancellation + handler.openInEditorFunc = mockEditor.GetEditorFunc() err := handler.createInteractive(ctx) - if err != nil { - t.Errorf("createInteractive should succeed even when cancelled: %v", err) - } + Expect.AssertNoError(t, err, "createInteractive should succeed even when cancelled") }) t.Run("handles editor error", func(t *testing.T) { - mockEditor := func(editor, filePath string) error { - return fmt.Errorf("editor failed to open") - } - - handler.openInEditorFunc = mockEditor + handler := NewHandlerTestHelper(t) + mockEditor := NewMockEditor().WithFailure("editor failed to open") + handler.openInEditorFunc = mockEditor.GetEditorFunc() err := handler.createInteractive(ctx) - if err == nil { - t.Error("createInteractive should fail when editor fails") - } - if !strings.Contains(err.Error(), "failed to open editor") { - t.Errorf("Expected editor error, got: %v", err) - } + Expect.AssertError(t, err, "failed to open editor", "createInteractive should fail when editor fails") }) t.Run("handles no editor configured", func(t *testing.T) { - oldEditor := os.Getenv("EDITOR") - oldPath := os.Getenv("PATH") - os.Unsetenv("EDITOR") - os.Setenv("PATH", "") - defer func() { - if oldEditor != "" { - os.Setenv("EDITOR", oldEditor) - } - os.Setenv("PATH", oldPath) - }() + handler := NewHandlerTestHelper(t) + envHelper := NewEnvironmentTestHelper() + defer envHelper.RestoreEnv() + + envHelper.UnsetEnv("EDITOR") + envHelper.SetEnv("PATH", "") err := handler.createInteractive(ctx) - if err == nil { - t.Error("createInteractive should fail when no editor is configured") - } - if !strings.Contains(err.Error(), "no editor configured") { - t.Errorf("Expected no editor error, got: %v", err) - } + Expect.AssertError(t, err, "no editor configured", "createInteractive should fail when no editor is configured") }) t.Run("handles file read error after editing", func(t *testing.T) { - mockEditor := func(editor, filePath string) error { - return os.Remove(filePath) - } - - handler.openInEditorFunc = mockEditor + handler := NewHandlerTestHelper(t) + mockEditor := NewMockEditor().WithFileDeleted() + handler.openInEditorFunc = mockEditor.GetEditorFunc() err := handler.createInteractive(ctx) - if err == nil { - t.Error("createInteractive should fail when temp file is deleted") - } - if !strings.Contains(err.Error(), "failed to read edited content") { - t.Errorf("Expected read error, got: %v", err) - } + Expect.AssertError(t, err, "failed to read edited content", "createInteractive should fail when temp file is deleted") }) }) } diff --git a/internal/handlers/test_utilities.go b/internal/handlers/test_utilities.go new file mode 100644 index 0000000..582c29d --- /dev/null +++ b/internal/handlers/test_utilities.go @@ -0,0 +1,345 @@ +package handlers + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stormlightlabs/noteleaf/internal/models" + "github.com/stormlightlabs/noteleaf/internal/store" +) + +// HandlerTestHelper wraps NoteHandler with test-specific functionality +type HandlerTestHelper struct { + *NoteHandler + tempDir string + cleanup func() +} + +// NewHandlerTestHelper creates a NoteHandler with isolated test database +func NewHandlerTestHelper(t *testing.T) *HandlerTestHelper { + tempDir, err := os.MkdirTemp("", "noteleaf-handler-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + oldConfigHome := os.Getenv("XDG_CONFIG_HOME") + os.Setenv("XDG_CONFIG_HOME", tempDir) + + cleanup := func() { + os.Setenv("XDG_CONFIG_HOME", oldConfigHome) + os.RemoveAll(tempDir) + } + + ctx := context.Background() + err = Setup(ctx, []string{}) + if err != nil { + cleanup() + t.Fatalf("Failed to setup database: %v", err) + } + + handler, err := NewNoteHandler() + if err != nil { + cleanup() + t.Fatalf("Failed to create note handler: %v", err) + } + + testHandler := &HandlerTestHelper{ + NoteHandler: handler, + tempDir: tempDir, + cleanup: cleanup, + } + + t.Cleanup(func() { + testHandler.Close() + testHandler.cleanup() + }) + + return testHandler +} + +// CreateTestNote creates a test note and returns its ID +func (th *HandlerTestHelper) CreateTestNote(t *testing.T, title, content string, tags []string) int64 { + ctx := context.Background() + note := &models.Note{ + Title: title, + Content: content, + Tags: tags, + Created: time.Now(), + Modified: time.Now(), + } + + id, err := th.repos.Notes.Create(ctx, note) + if err != nil { + t.Fatalf("Failed to create test note: %v", err) + } + return id +} + +// CreateTestFile creates a temporary markdown file with content +func (th *HandlerTestHelper) CreateTestFile(t *testing.T, filename, content string) string { + filePath := filepath.Join(th.tempDir, filename) + err := os.WriteFile(filePath, []byte(content), 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + return filePath +} + +// MockEditor provides a mock editor function for testing +type MockEditor struct { + shouldFail bool + failureMsg string + contentToWrite string + deleteFile bool + makeReadOnly bool +} + +// NewMockEditor creates a mock editor with default success behavior +func NewMockEditor() *MockEditor { + return &MockEditor{ + contentToWrite: `# Test Note + +Test content here. + +`, + } +} + +// WithFailure configures the mock editor to fail +func (me *MockEditor) WithFailure(msg string) *MockEditor { + me.shouldFail = true + me.failureMsg = msg + return me +} + +// WithContent configures the content the mock editor will write +func (me *MockEditor) WithContent(content string) *MockEditor { + me.contentToWrite = content + return me +} + +// WithFileDeleted configures the mock editor to delete the temp file +func (me *MockEditor) WithFileDeleted() *MockEditor { + me.deleteFile = true + return me +} + +// WithReadOnly configures the mock editor to make the file read-only +func (me *MockEditor) WithReadOnly() *MockEditor { + me.makeReadOnly = true + return me +} + +// GetEditorFunc returns the editor function for use with NoteHandler +func (me *MockEditor) GetEditorFunc() editorFunc { + return func(editor, filePath string) error { + if me.shouldFail { + return fmt.Errorf("%s", me.failureMsg) + } + + if me.deleteFile { + return os.Remove(filePath) + } + + if me.makeReadOnly { + os.Chmod(filePath, 0444) + return nil + } + + return os.WriteFile(filePath, []byte(me.contentToWrite), 0644) + } +} + +// DatabaseTestHelper provides database testing utilities +type DatabaseTestHelper struct { + originalDB *store.Database + handler *HandlerTestHelper +} + +// NewDatabaseTestHelper creates a helper for database error testing +func NewDatabaseTestHelper(handler *HandlerTestHelper) *DatabaseTestHelper { + return &DatabaseTestHelper{ + originalDB: handler.db, + handler: handler, + } +} + +// CloseDatabase closes the database connection +func (dth *DatabaseTestHelper) CloseDatabase() { + dth.handler.db.Close() +} + +// RestoreDatabase restores the original database connection +func (dth *DatabaseTestHelper) RestoreDatabase(t *testing.T) { + var err error + dth.handler.db, err = store.NewDatabase() + if err != nil { + t.Fatalf("Failed to restore database: %v", err) + } +} + +// DropNotesTable drops the notes table to simulate database errors +func (dth *DatabaseTestHelper) DropNotesTable() { + dth.handler.db.Exec("DROP TABLE notes") +} + +// CreateCorruptedDatabase creates a new database with corrupted schema +func (dth *DatabaseTestHelper) CreateCorruptedDatabase(t *testing.T) { + dth.CloseDatabase() + + db, err := store.NewDatabase() + if err != nil { + t.Fatalf("Failed to create corrupted database: %v", err) + } + + // Drop the notes table to simulate corruption + db.Exec("DROP TABLE notes") + dth.handler.db = db +} + +// AssertionHelpers provides test assertion utilities +type AssertionHelpers struct{} + +// AssertError checks that an error occurred and optionally contains expected text +func (ah AssertionHelpers) AssertError(t *testing.T, err error, expectedSubstring string, msg string) { + t.Helper() + if err == nil { + t.Errorf("%s: expected error but got none", msg) + return + } + if expectedSubstring != "" && !containsString(err.Error(), expectedSubstring) { + t.Errorf("%s: expected error containing %q, got: %v", msg, expectedSubstring, err) + } +} + +// AssertNoError checks that no error occurred +func (ah AssertionHelpers) AssertNoError(t *testing.T, err error, msg string) { + t.Helper() + if err != nil { + t.Errorf("%s: unexpected error: %v", msg, err) + } +} + +// AssertNoteExists checks that a note exists in the database +func (ah AssertionHelpers) AssertNoteExists(t *testing.T, handler *HandlerTestHelper, id int64) { + t.Helper() + ctx := context.Background() + _, err := handler.repos.Notes.Get(ctx, id) + if err != nil { + t.Errorf("Note %d should exist but got error: %v", id, err) + } +} + +// AssertNoteNotExists checks that a note does not exist in the database +func (ah AssertionHelpers) AssertNoteNotExists(t *testing.T, handler *HandlerTestHelper, id int64) { + t.Helper() + ctx := context.Background() + _, err := handler.repos.Notes.Get(ctx, id) + if err == nil { + t.Errorf("Note %d should not exist but was found", id) + } +} + +// EnvironmentTestHelper provides environment manipulation utilities for testing +type EnvironmentTestHelper struct { + originalVars map[string]string +} + +// NewEnvironmentTestHelper creates a new environment test helper +func NewEnvironmentTestHelper() *EnvironmentTestHelper { + return &EnvironmentTestHelper{ + originalVars: make(map[string]string), + } +} + +// SetEnv sets an environment variable and remembers the original value +func (eth *EnvironmentTestHelper) SetEnv(key, value string) { + if _, exists := eth.originalVars[key]; !exists { + eth.originalVars[key] = os.Getenv(key) + } + os.Setenv(key, value) +} + +// UnsetEnv unsets an environment variable and remembers the original value +func (eth *EnvironmentTestHelper) UnsetEnv(key string) { + if _, exists := eth.originalVars[key]; !exists { + eth.originalVars[key] = os.Getenv(key) + } + os.Unsetenv(key) +} + +// RestoreEnv restores all modified environment variables +func (eth *EnvironmentTestHelper) RestoreEnv() { + for key, value := range eth.originalVars { + if value == "" { + os.Unsetenv(key) + } else { + os.Setenv(key, value) + } + } +} + +// Helper function to check if string contains substring (case-insensitive) +func containsString(haystack, needle string) bool { + if needle == "" { + return true + } + return len(haystack) >= len(needle) && + haystack[len(haystack)-len(needle):] == needle || + haystack[:len(needle)] == needle || + (len(haystack) > len(needle) && + func() bool { + for i := 1; i <= len(haystack)-len(needle); i++ { + if haystack[i:i+len(needle)] == needle { + return true + } + } + return false + }()) +} + +// FileTestHelper provides file manipulation utilities for testing +type FileTestHelper struct { + tempFiles []string +} + +// NewFileTestHelper creates a new file test helper +func NewFileTestHelper() *FileTestHelper { + return &FileTestHelper{} +} + +// CreateTempFile creates a temporary file with content +func (fth *FileTestHelper) CreateTempFile(t *testing.T, pattern, content string) string { + file, err := os.CreateTemp("", pattern) + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + + if content != "" { + if _, err := file.WriteString(content); err != nil { + file.Close() + os.Remove(file.Name()) + t.Fatalf("Failed to write temp file content: %v", err) + } + } + + fileName := file.Name() + file.Close() + + fth.tempFiles = append(fth.tempFiles, fileName) + return fileName +} + +// Cleanup removes all temporary files created by this helper +func (fth *FileTestHelper) Cleanup() { + for _, file := range fth.tempFiles { + os.Remove(file) + } + fth.tempFiles = nil +} + +var Expect = AssertionHelpers{} diff --git a/internal/services/samples/movie.html b/internal/services/samples/movie.html index bfe9b93..f0d75c7 100644 --- a/internal/services/samples/movie.html +++ b/internal/services/samples/movie.html @@ -1,3832 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The Fantastic Four: First Steps | Rotten Tomatoes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - -
- - - - -
-
- - - -
-
- - - - - - - - - - -
- - ☰ - - - -
-
-
-
- - - Rotten Tomatoes - -
- -
-
- - - - - - - - - - - - - - Cancel - - - - - - - - - - - - - - - Movies - - - - - - - Tv shows - - - - - - - RT App - - - - - - - - - News - - - - - - - Showtimes - - - - -
- - - - - - - -
-
- - - - -
- - -
- -
-

- The Fantastic Four: First Steps -

- - - Play trailer - - 0:59 - - - - - Poster for - - - The Fantastic Four: First Steps - - - - PG-13 - - Now Playing - - 1h 54m - - - - Action - - Adventure - - Sci-Fi - - Fantasy - - - - Play Trailer - - - Watchlist - - - - -
- - - - - Watchlist - - - - - - Tomatometer - - - - - Popcornmeter -
- - -
- - - - - - -
- -
-
- -
- - - - - - 87% - Tomatometer - - 390 Reviews - - - - - - 91% - Popcornmeter - - 10,000+ Verified Ratings - - -
- - - Set against the vibrant backdrop of a 1960s-inspired, retro-futuristic world, Marvel - Studios' "The Fantastic Four: First Steps" introduces Marvel's First - Family--Reed Richards/Mister Fantastic (Pedro Pascal), Sue Storm/Invisible Woman (Vanessa - Kirby), Johnny Storm/Human Torch (Joseph Quinn) and Ben Grimm/The Thing (Ebon Moss-Bachrach) as - they face their most daunting challenge yet. Forced to balance their roles as heroes with the - strength of their family bond, they must defend Earth from a ravenous space god called Galactus - (Ralph Ineson) and his enigmatic Herald, Silver Surfer (Julia Garner). And if Galactus' plan - to devour the entire planet and everyone on it weren't bad enough, it suddenly gets very - personal. - - - - - - - - -
- - - - - - Now in Theaters - Now Playing - - Buy Tickets - -
- -
- - - - -
- - -
- - - -
- - -
- -
- - - -
-
- - The Fantastic Four: First Steps - -

- What to Know -

-
- -
- - - - -
- - - Critics Consensus - -

Benefitting from rock-solid cast chemistry and clad in appealingly retro 1960s design, this - crack at The Fantastic Four does Marvel's First Family justice.

- Read Critics Reviews -
- - - -
- - - -
- - - Audience Says - -

The Fantastic Four takes the world by Storm, Thing, Reed, Johnny and baby, forging a - new path for this bespoke family that, with these First Steps, leaps into cosmic action - with retro-futuristic verve.

- Read Audience Reviews -
- -
-
- -
- - - - - - - - -
- -
- - - -
-
-

- Critics Reviews -

- - View More (390) - -
- -
- - - - - - - - Dwight Brown - - - - - DwightBrownInk.com - - - - - Audiences shouldn’t have to sacrifice eye-catching stunts and imagery for great writing and - acting. They should have it all. Should but won’t in this case. - - - - - - Rated: 2.5/4 - - - Aug 18, 2025 - - Full Review - - - - - - - Sergio Burstein - - - - - Los Angeles Times - - - - - For now, let's enjoy what this efficient commercial product has to offer, which - surprisingly lasts less than two hours... [Full review in Spanish] - - - - - - - - - Aug 11, 2025 - - Full - Review - - - - - - - Mark Kermode - - - - - Kermode and Mayo's Take (YouTube) - - - - - Even if you don't like the film, it is absolutely worth it for the furniture. - - - - - - - - - Aug 6, 2025 - - Full Review - - - - - - - Kevin Carr - - - - - Fat Guys at the Movies - - - - - It’s still a superhero movie, and it connects to the next phase of Marvel crossover films. - However, it taps into some real human elements. - - - - - - Rated: 4/5 - - - Sep 4, 2025 - - Full - Review - - - - - - - Niall McCloskey - - - - - Film Ireland Magazine - - - - - If you’re looking for an electric superhero thrilling ride filled with compelling - performances, great action and that classic comic flair, this may be four you. - - - - - - - - - Sep 3, 2025 - - Full - Review - - - - - - - Troy Ribeiro - - - - - Free Press Journal (India) - - - - - In The Fantastic Four: First Steps, Marvel returns to the drawing board—this time armed with - chalk, retro optimism, and the ever-dependable Pedro Pascal - - - - - - Rated: 2.5/5 - - - Aug 30, 2025 - - Full - Review - - - - - Read all reviews - - - -
-
- -
- - - - -
-
-

- Audience Reviews -

- - View More (1000+) - -
- -
- - - - - Sanjeev - - - - Fantastic4 was okay. Choppy character development. Choppy story. - - - - - Rated 2 out of 5 stars - - - 09/07/25 - - - Full Review - - - - - - Daniel - - - - Could have benefitted by adding battles with Mole Man and a few of the older FF4 rogues - gallery - - - - - Rated 4 out of 5 stars - - - 09/07/25 - - - Full Review - - - - - - Valentina - - - - The whole family enjoyed it 😃 - - - - - Rated 4 out of 5 stars - - - 09/07/25 - - - Full Review - - - - - - DUANE S - - - - It was surprisingly entertaining - - - - - Rated 4.5 out of 5 stars - - - 09/07/25 - - - Full Review - - - - - - Tish - - - - it was good but I feel like I missed something. - - - - - Rated 4 out of 5 stars - - - 09/07/25 - - - Full Review - - - - - - David L - - - - Way too much back story and not enough action. Kept waiting for something to happen. - - - - - Rated 1 out of 5 stars - - - 09/07/25 - - - Full Review - - - - - - Read all reviews - - - - -
- - - - -
- - - -
- - - - -
- - The Fantastic Four: First Steps - -

- My Rating -

-
- -
- - - - - - - Read More - Read Less - - - - POST RATING - WRITE A REVIEW - EDIT REVIEW - -
- - -
- - - - - -
- -
- - - -
-
- -

- - The Fantastic Four: First Steps - -

-
- - - - - - - The Fantastic Four: First Steps: Movie Clip - I Herald Galactus - - - - The Fantastic Four: - First Steps: Movie Clip - I Herald Galactus - - - - 1:01 - - - - - - - The Fantastic Four: First Steps: Movie Clip - Sunday Dinner - - - - The Fantastic Four: - First Steps: Movie Clip - Sunday Dinner - - - - 1:24 - - - - - - View more videos - - - -
- -
- - -
- -
- - - -
-
- -
- -
- - - - - Godzilla vs. Kong - - - - - 76% - - - - 91% - - - Godzilla vs. Kong - - - Watchlist - - - - - TRAILER - for Godzilla vs. Kong - - - - - - - Avengers: Infinity War - - - - - 85% - - - - 92% - - - Avengers: Infinity War - - - Watchlist - - - - - TRAILER - for Avengers: Infinity War - - - - - - - War for the Planet of the Apes - - - - - 94% - - - - 84% - - - War for the Planet of the Apes - - - Watchlist - - - - - TRAILER - for War for the Planet of the Apes - - - - - - - Godzilla Minus One - - - - - 99% - - - - 98% - - - Godzilla Minus One - - - Watchlist - - - - - TRAILER - for Godzilla Minus One - - - - - - - The Creator - - - - - 67% - - - - 75% - - - The Creator - - - Watchlist - - - - - TRAILER - for The Creator - - - - - - - - - Discover more movies and TV shows. - - View More - - - -
-
- -
- - - - - -
- -
- - - - - -
- - -
- -
- - - - - -
- - - - - - - - -
- -
- - - -
-
-

- - Movie Info - -

-
- -
- -
- Synopsis - Set against the vibrant backdrop of a 1960s-inspired, - retro-futuristic world, Marvel Studios' "The Fantastic Four: First Steps" - introduces Marvel's First Family--Reed Richards/Mister Fantastic (Pedro Pascal), Sue - Storm/Invisible Woman (Vanessa Kirby), Johnny Storm/Human Torch (Joseph Quinn) and Ben Grimm/The - Thing (Ebon Moss-Bachrach) as they face their most daunting challenge yet. Forced to balance - their roles as heroes with the strength of their family bond, they must defend Earth from a - ravenous space god called Galactus (Ralph Ineson) and his enigmatic Herald, Silver Surfer (Julia - Garner). And if Galactus' plan to devour the entire planet and everyone on it weren't - bad enough, it suddenly gets very personal. -
- - -
- -
-
- Director -
-
- - Matt Shakman - -
-
- -
-
- Producer -
-
- - Kevin Feige - -
-
- -
-
- Screenwriter -
-
- - Peter Cameron - -
-
- -
-
- Distributor -
-
- - Walt Disney Pictures - -
-
- -
-
- Production Co -
-
- - Marvel Studios - -
-
- -
-
- Rating -
-
- - PG-13 (Some Language|Action/Violence) - -
-
- -
-
- Genre -
-
- - Action, - - Adventure, - - Sci-Fi, - - Fantasy - -
-
- -
-
- Original Language -
-
- - English - -
-
- -
-
- Release Date (Theaters) -
-
- - Jul 25, 2025, Wide - -
-
- -
-
- Box Office (Gross USA) -
-
- - $266.4M - -
-
- -
-
- Runtime -
-
- - 1h 54m - -
-
- -
-
- Sound Mix -
-
- - Dolby Atmos - -
-
- -
-
- Aspect Ratio -
-
- - Digital 2.39:1 - -
-
- -
-
-
- -
- - -
- - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +The Fantastic Four: First Steps | Rotten Tomatoes
☰
Rotten Tomatoes
Cancel Movies Tv shows RT App News Showtimes

The Fantastic Four: First Steps

Play trailer0:59Poster for The Fantastic Four: First StepsPG-13Now Playing1h 54mActionAdventureSci-FiFantasyPlayTrailer Watchlist
WatchlistTomatometerPopcornmeter
87%Tomatometer390 Reviews 91%Popcornmeter10,000+ Verified Ratings
Set against the vibrant backdrop of a 1960s-inspired, retro-futuristic world, Marvel Studios' "The Fantastic Four: First Steps" introduces Marvel's First Family--Reed Richards/Mister Fantastic (Pedro Pascal), Sue Storm/Invisible Woman (Vanessa Kirby), Johnny Storm/Human Torch (Joseph Quinn) and Ben Grimm/The Thing (Ebon Moss-Bachrach) as they face their most daunting challenge yet. Forced to balance their roles as heroes with the strength of their family bond, they must defend Earth from a ravenous space god called Galactus (Ralph Ineson) and his enigmatic Herald, Silver Surfer (Julia Garner). And if Galactus' plan to devour the entire planet and everyone on it weren't bad enough, it suddenly gets very personal.
Now in TheatersNow PlayingBuy Tickets
The Fantastic Four: First Steps

What to Know

Critics Consensus

Benefitting from rock-solid cast chemistry and clad in appealingly retro 1960s design, this crack at The Fantastic Fourdoes Marvel's First Family justice.

Read Critics Reviews

Audience Says

The Fantastic Fourtakes the world by Storm, Thing, Reed, Johnny and baby, forging a new path for this bespoke family that, with these First Steps, leaps into cosmic action with retro-futuristic verve.

Read Audience Reviews

Critics Reviews

View More (390)
Dwight Brown DwightBrownInk.com Audiences shouldn’t have to sacrifice eye-catching stunts and imagery for great writing and acting. They should have it all. Should but won’t in this case. Rated: 2.5/4Aug 18, 2025 Full ReviewSergio Burstein Los Angeles Times For now, let's enjoy what this efficient commercial product has to offer, which surprisingly lasts less than two hours... [Full review in Spanish] Aug 11, 2025 Full ReviewMark Kermode Kermode and Mayo's Take (YouTube) Even if you don't like the film, it is absolutely worth it for the furniture. Aug 6, 2025 Full ReviewKevin Carr Fat Guys at the Movies It’s still a superhero movie, and it connects to the next phase of Marvel crossover films. However, it taps into some real human elements. Rated: 4/5Sep 4, 2025 Full ReviewNiall McCloskey Film Ireland Magazine If you’re looking for an electric superhero thrilling ride filled with compelling performances, great action and that classic comic flair, this may be four you. Sep 3, 2025 Full ReviewTroy Ribeiro Free Press Journal (India) In The Fantastic Four: First Steps, Marvel returns to the drawing board—this time armed with chalk, retro optimism, and the ever-dependable Pedro Pascal Rated: 2.5/5Aug 30, 2025 Full ReviewRead all reviews

Audience Reviews

View More (1000+)
Sanjeev Fantastic4 was okay. Choppy character development. Choppy story. Rated 2 out of 5 stars09/07/25Full Review Daniel Could have benefitted by adding battles with Mole Man and a few of the older FF4 rogues gallery Rated 4 out of 5 stars09/07/25Full Review Valentina The whole family enjoyed it 😃 Rated 4 out of 5 stars09/07/25Full Review DUANE S It was surprisingly entertaining Rated 4.5 out of 5 stars09/07/25Full Review Tish it was good but I feel like I missed something. Rated 4 out of 5 stars09/07/25Full Review David L Way too much back story and not enough action. Kept waiting for something to happen. Rated 1 out of 5 stars09/07/25Full Review Read all reviews
The Fantastic Four: First Steps

My Rating

Read MoreRead LessPOST RATING WRITE A REVIEW EDIT REVIEW

The Fantastic Four: First Steps

The Fantastic Four: First Steps: Movie Clip - I Herald GalactusThe Fantastic Four: First Steps: Movie Clip - I Herald Galactus1:01 The Fantastic Four: First Steps: Movie Clip - Sunday DinnerThe Fantastic Four: First Steps: Movie Clip - Sunday Dinner1:24 View more videos
Godzilla vs. Kong76% 91% Godzilla vs. Kong WatchlistTRAILERfor Godzilla vs. KongAvengers: Infinity War85% 92% Avengers: Infinity War WatchlistTRAILERfor Avengers: Infinity WarWar for the Planet of the Apes94% 84% War for the Planet of the Apes WatchlistTRAILERfor War for the Planet of the ApesGodzilla Minus One99% 98% Godzilla Minus One WatchlistTRAILERfor Godzilla Minus OneThe Creator67% 75% The Creator WatchlistTRAILERfor The CreatorDiscover more movies and TV shows.View More

Movie Info

SynopsisSet against the vibrant backdrop of a 1960s-inspired, retro-futuristic world, Marvel Studios' "The Fantastic Four: First Steps" introduces Marvel's First Family--Reed Richards/Mister Fantastic (Pedro Pascal), Sue Storm/Invisible Woman (Vanessa Kirby), Johnny Storm/Human Torch (Joseph Quinn) and Ben Grimm/The Thing (Ebon Moss-Bachrach) as they face their most daunting challenge yet. Forced to balance their roles as heroes with the strength of their family bond, they must defend Earth from a ravenous space god called Galactus (Ralph Ineson) and his enigmatic Herald, Silver Surfer (Julia Garner). And if Galactus' plan to devour the entire planet and everyone on it weren't bad enough, it suddenly gets very personal.
Director
Matt Shakman
Producer
Kevin Feige
Screenwriter
Peter Cameron
Distributor
Walt Disney Pictures
Production Co
Marvel Studios
Rating
PG-13 (Some Language|Action/Violence)
Genre
Action, Adventure, Sci-Fi, Fantasy
Original Language
English
Release Date (Theaters)
Jul 25, 2025, Wide
Box Office (Gross USA)
$266.4M
Runtime
1h 54m
Sound Mix
Dolby Atmos
Aspect Ratio
Digital 2.39:1
diff --git a/internal/services/samples/movie_search.html b/internal/services/samples/movie_search.html index 2a62dc5..b313409 100644 --- a/internal/services/samples/movie_search.html +++ b/internal/services/samples/movie_search.html @@ -1,2194 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Search Results | Rotten Tomatoes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - -
- - - - -
-
- - - -
-
- - - - - - - - - - -
- - ☰ - - - -
-
-
-
- - - Rotten Tomatoes - -
- -
-
- - - - - - - - - - - - - - Cancel - - - - - - - - - - - - - - - Movies - - - - - - - Tv shows - - - - - - - RT App - - - - - - - - - News - - - - - - - Showtimes - - - - -
- - - - - - - -
-
- - - - - -
-
- -

- Search Results for : "Fantastic Four" -

- - - - -
- -
-
- -
-
- -
- - -
- - -
- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Search Results | Rotten Tomatoes
☰
Rotten Tomatoes
Cancel Movies Tv shows RT App News Showtimes
diff --git a/internal/services/samples/search.html b/internal/services/samples/search.html index d8618d1..2e9ff23 100644 --- a/internal/services/samples/search.html +++ b/internal/services/samples/search.html @@ -1,2154 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Search Results | Rotten Tomatoes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - -
- - - - -
-
- - - -
-
- - - - - - - - - - -
- - ☰ - - - -
-
-
-
- - - Rotten Tomatoes - -
- -
-
- - - - - - - - - - - - - - Cancel - - - - - - - - - - - - - - - Movies - - - - - - - Tv shows - - - - - - - RT App - - - - - - - - - News - - - - - - - Showtimes - - - - -
- - - - - - - -
-
- - - - - -
-
- -

- Search Results for : "peacemaker" -

- - -
- - - - -

- Movies -

- - - - -
- - - - -

- TV shows -

- - - - -
- - - - - - -

- Celebrities -

- - - - -
- -
- -
- -
-
- -
-
- -
- - -
- - -
- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Search Results | Rotten Tomatoes
☰
Rotten Tomatoes
Cancel Movies Tv shows RT App News Showtimes
diff --git a/internal/services/samples/series_overview.html b/internal/services/samples/series_overview.html index 07d710d..085f86e 100644 --- a/internal/services/samples/series_overview.html +++ b/internal/services/samples/series_overview.html @@ -1,3150 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Peacemaker (2022) | Rotten Tomatoes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - -
- - - - -
-
- - - -
-
- - - - - - - - - - -
- - ☰ - - - -
-
-
-
- - - Rotten Tomatoes - -
- -
-
- - - - - - - - - - - - - - Cancel - - - - - - - - - - - - - - - Movies - - - - - - - Tv shows - - - - - - - RT App - - - - - - - - - News - - - - - - - Showtimes - - - - -
- - - - - - - -
-
- - - - -
- - -
- -
-

- Peacemaker -

- - - Play trailer - - 1:39 - - - - - Poster for - - - Peacemaker - - - - TV-MA - - Next Ep Thu Sep 11 - - 2 Seasons - - - - Comedy - - Action - - - - Play Trailer - - - Watchlist - - - - -
- - - - - Watchlist - - - - - - Tomatometer - - - - - Popcornmeter -
- - -
- - - - - - -
- -
-
- -
- - - - - - 96% - Avg. Tomatometer - - 150 Reviews - - - - - - 84% - Avg. Popcornmeter - - 2,500+ Ratings - - -
- - - A man fights for peace at any cost, no matter how many people he has to kill to get it. - - - - - - - - -
- - - - - - Watch on Fandango at Home - - - Stream Now - -
- -
- - - - -
- - -
- - - -
- - -
- -
- - - -
-
-

- Where to - Watch -

-

- - Peacemaker - -

-
- - - - - - - -
- -
- - -
- -
- - - - -
-
-

- Seasons -

-
- -
- - - - - Season 2 - - 99% - Next Ep Thu Sep 11 - - Details - - - - - - Season 1 - - 93% - 2022 - - Details - - - - -
-
- -
- - - - - - - - - - - -
- -
- - - -
-
- -
- -
- - - - - Twisted Metal - - - - - 79% - - - - 88% - - - Twisted Metal - - - Watchlist - - - - - TRAILER - for Twisted Metal - - - - - - - Comrade Detective - - - - - 85% - - - - 88% - - - Comrade Detective - - - Watchlist - - - - - - - Saul of the Mole Men - - - - - 17% - - - - % - - - Saul of the Mole Men - - - Watchlist - - - - - - - Somebody Somewhere - - - - - 100% - - - - 93% - - - Somebody Somewhere - - - Watchlist - - - - - TRAILER - for Somebody Somewhere - - - - - - - South Side - - - - - 100% - - - - 92% - - - South Side - - - Watchlist - - - - - TRAILER - for South Side - - - - - - - - - Discover more movies and TV shows. - - View More - - - -
-
- -
- - - - - -
- -
- - - - - -
- - -
- -
- - - - - -
- - - - - - - - -
- -
- - - -
-
-

- - Series Info - -

-
- -
- -
- Synopsis - A man fights for peace at any cost, no matter how many people he - has to kill to get it. -
- - -
- -
-
- Executive Producer -
-
- - James Gunn, - - Peter Safran, - - John Cena - -
-
- -
-
- Screenwriter -
-
- - James Gunn - -
-
- -
-
- Network -
-
- - HBO Max - -
-
- -
-
- Rating -
-
- - TV-MA - -
-
- -
-
- Genre -
-
- - Comedy, - - Action - -
-
- -
-
- Original Language -
-
- - English - -
-
- -
-
- Release Date -
-
- - Jan 13, 2022 - -
-
- -
-
-
- -
- - -
- - - - - - -
-
- - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Peacemaker (2022) | Rotten Tomatoes
☰
Rotten Tomatoes
Cancel Movies Tv shows RT App News Showtimes

Peacemaker

Play trailer1:39Poster for PeacemakerTV-MANext Ep Thu Sep 112 SeasonsComedyActionPlayTrailer Watchlist
WatchlistTomatometerPopcornmeter
96%Avg. Tomatometer150 Reviews 84%Avg. Popcornmeter2,500+ Ratings
A man fights for peace at any cost, no matter how many people he has to kill to get it.
Watch on Fandango at HomeStream Now

Where to Watch

Peacemaker

Seasons

Season 299%Next Ep Thu Sep 11Details Season 193%2022Details
Twisted Metal79% 88% Twisted Metal WatchlistTRAILERfor Twisted MetalComrade Detective85% 88% Comrade Detective WatchlistSaul of the Mole Men17% % Saul of the Mole Men WatchlistSomebody Somewhere100% 93% Somebody Somewhere WatchlistTRAILERfor Somebody SomewhereSouth Side100% 92% South Side WatchlistTRAILERfor South SideDiscover more movies and TV shows.View More

Series Info

SynopsisA man fights for peace at any cost, no matter how many people he has to kill to get it.
Executive Producer
James Gunn, Peter Safran, John Cena
Screenwriter
James Gunn
Network
HBO Max
Rating
TV-MA
Genre
Comedy, Action
Original Language
English
Release Date
Jan 13, 2022
diff --git a/internal/services/samples/series_season.html b/internal/services/samples/series_season.html index ed7cc31..1207506 100644 --- a/internal/services/samples/series_season.html +++ b/internal/services/samples/series_season.html @@ -1,3888 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Peacemaker: Season 2 | Rotten Tomatoes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - -
- - - - -
-
- - - -
-
- - - - - - - - - - -
- - ☰ - - - -
-
-
-
- - - Rotten Tomatoes - -
- -
-
- - - - - - - - - - - - - - Cancel - - - - - - - - - - - - - - - Movies - - - - - - - Tv shows - - - - - - - RT App - - - - - - - - - News - - - - - - - Showtimes - - - - -
- - - - - - - -
-
- - - - -
- - -
- - - - -
-

- Season 2 – Peacemaker -

- - - Play trailer - - 1:39 - - - - - Poster for - - - Season 2 – Peacemaker - - - - Next Ep Thu Sep 11 - - - - Comedy - - Action - - - - Play Trailer - - - Watchlist - - - - -
- - - - - Watchlist - - - - - - Tomatometer - - - - - Popcornmeter -
- - -
- - - - - - -
- -
-
- -
- - - - - - 99% - Tomatometer - - 82 Reviews - - - - - - 80% - Popcornmeter - - 1,000+ Ratings - - -
- - - A man fights for peace at any cost, no matter how many people he has to kill to get it. - - - - - - - - -
- - - - - - Watch on Max - - - Stream Now - -
- -
- - - - -
- - -
- - - -
- - -
- -
- - - -
-
-

- Where to - Watch -

-

- - Peacemaker — Season 2 - -

-
- - - - - - - -
- -
- - -
- -
- - - -
-
- - Peacemaker — Season 2 - -

- What to Know -

-
- -
- - - - -
- - - Critics Consensus - -

Peacemaker's second season goes multidimensional while still maintaining a singular - focus on emotional stakes, seamlessly transporting this outrageous antihero into a fresh - cinematic universe.

- Read Critics Reviews -
- - - - - -
-
- -
- - - - - - - - -
- -
- - - -
-
-

- Critics Reviews -

- - View More (82) - -
- -
- - - - - - - - Craig Mathieson - - - - - The Age (Australia) - - - - - Dimensional travel, vengeful relatives and spurts of bloody violence set the tone for a - series that remains a cartoonish change of pace. - - - - - - Rated: 3/5 - - - Aug 28, 2025 - - Full - Review - - - - - - - Graeme Virtue - - - - - Guardian - - - - - Gunn clearly loved these damaged characters enough to carry them over to his shiny new - universe, but he is also never afraid to put them through the wringer. It makes even the - tiniest victories in their lives feel momentous. - - - - - - Rated: 4/5 - - - Aug 25, 2025 - - Full - Review - - - - - - - Sam Adams - - - - - Slate - - - - - Like the first season, the second season of Peacemaker risks stretching too few jokes over - too many hours—even if sometimes the joke going on for too long is the joke. But it’s also - strangely fascinating. - - - - - - - - - Aug 25, 2025 - - Full - Review - - - - - - - David Craig - - - - - Radio Times - - - - - From the opening dance number, it's an enormous amount of fun that carefully balances - its surreal pleasures with impactful character-led moments – and plenty of unexpected - twists. - - - - - - Rated: 4/5 - - - Aug 30, 2025 - - Full - Review - - - - - - - Alex Maidy - - - - - JoBlo's Movie Network - - - - - While I do not suffer from bird blindness, I can say that Peacemaker soars with the ducks. - - - - - - Rated: 9/10 - - - Aug 28, 2025 - - Full - Review - - - - - - - Joel Keller - - - - - Decider - - - - - Peacemaker continues to be a funny but emotional superhero drama, with a surprisingly - effective performance by Cena at its center, with a fun-to-watch ensemble around him. - - - - - - - - - Aug 25, 2025 - - Full - Review - - - - - Read all reviews - - - -
-
- -
- - - - -
-
-

- Audience Reviews -

- - View More (330) - -
- -
- - - - - Andres G - - - - No es mala pero tampoco no es una de las mejores series como dice la critica - - - - - Rated 3.5 out of 5 stars - - - 09/07/25 - - - Full Review - - - - - - Jude C - - - - Absolute-cinema, it's too bad we can't see peacemaker though - - - - - Rated 5 out of 5 stars - - - 09/07/25 - - - Full Review - - - - - - Łukasz S - - - - - - - - - - - - - - - - - Good - - - - - - - - - Rated 5 out of 5 stars - - - 09/06/25 - - - Full Review - - - - - - Fernando N - - - - Essa segunda temporada está me surpreendendo positivamente, a química dos personagens estão - incríveis, cenas de ação perfeitas e as participações surpreendentes - - - - - Rated 5 out of 5 stars - - - 09/06/25 - - - Full Review - - - - - - Shan V - - - - starting to introduce old characters like theyre somehow important to the DCU, thge comedy - puts me off and reminds me of the beginnings of Marvel shows where they started out good then - went to crap. - - - - - Rated 0.5 out of 5 stars - - - 09/07/25 - - - Full Review - - - - - - Zubair A - - - - With Gunn at the helm, the mission continues: flawless action, killer music, and jokes that - hit like a helmet-beam. Fabulous doesn't even cover it. - - - - - Rated 5 out of 5 stars - - - 09/05/25 - - - Full Review - - - - - - Read all reviews - - - - -
- - - - -
- - - -
- - - - -
- - Peacemaker — Season 2 - -

- My Rating -

-
- -
- - - - - - - Read More - Read Less - - - - POST RATING - WRITE A REVIEW - EDIT REVIEW - -
- - -
- - - - - -
- -
- - - - -
-
-

- Episodes -

-
- -
- - - - - Episode 1 - Aired Aug 21, 2025 - The Ties That - Grind - While Peacemaker attempts - to join the Justice Gang, Harcourt struggles to find work, and Economos takes on a challenging - new assignment. - - Details - - - - - - Episode 2 - Aired Aug 28, 2025 - A Man Is Only as Good as - His Bird - As Economos clashes with - his new handler, Peacemaker must deal with the consequences of his actions in the alternate - dimension. - - Details - - - - - - Episode 3 - Aired Sep 4, 2025 - Another Rick Up My - Sleeve - A man fights for peace at - any cost, no matter how many people he has to kill to get it. - - Details - - - - - - Episode 4 - Airs Thu Sep 11 - Need I Say Door - A man fights for peace at - any cost, no matter how many people he has to kill to get it. - - Details - - - - - - Episode 5 - Airs Thu Sep 18 - Back to the - Suture - A man fights for peace at - any cost, no matter how many people he has to kill to get it. - - Details - - - - - - Episode 6 - Airs Thu Sep 25 - Ignorance is - Chris - A man fights for peace at - any cost, no matter how many people he has to kill to get it. - - Details - - - - - - Episode 7 - Airs Thu Oct 2 - - A man fights for peace at - any cost, no matter how many people he has to kill to get it. - - Details - - - - - - Episode 8 - Airs Thu Oct 9 - - A man fights for peace at - any cost, no matter how many people he has to kill to get it. - - Details - - - - -
-
- -
- - -
- -
- - - -
-
- -
- -
- - - - - Twisted Metal - - - - - 79% - - - - 88% - - - Twisted Metal - - - Watchlist - - - - - TRAILER - for Twisted Metal - - - - - - - Comrade Detective - - - - - 85% - - - - 88% - - - Comrade Detective - - - Watchlist - - - - - - - Saul of the Mole Men - - - - - 17% - - - - % - - - Saul of the Mole Men - - - Watchlist - - - - - - - Somebody Somewhere - - - - - 100% - - - - 93% - - - Somebody Somewhere - - - Watchlist - - - - - TRAILER - for Somebody Somewhere - - - - - - - South Side - - - - - 100% - - - - 92% - - - South Side - - - Watchlist - - - - - TRAILER - for South Side - - - - - - - - - Discover more movies and TV shows. - - View More - - - -
-
- -
- - - - - -
- -
- - - - - -
- - -
- -
- - - - - -
- - - - - - - - -
- -
- - - -
-
-

- - Season Info - -

-
- -
- - -
- -
-
- Executive Producer -
-
- - James Gunn, - - Peter Safran, - - John Cena - -
-
- -
-
- Screenwriter -
-
- - James Gunn - -
-
- -
-
- Network -
-
- - HBO Max - -
-
- -
-
- Rating -
-
- - TV-MA - -
-
- -
-
- Genre -
-
- - Comedy, - - Action - -
-
- -
-
- Original Language -
-
- - English - -
-
- -
-
- Release Date -
-
- - Aug 21, 2025 - -
-
- -
-
-
- -
- - -
- - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Peacemaker: Season 2 | Rotten Tomatoes
☰
Rotten Tomatoes
Cancel Movies Tv shows RT App News Showtimes

Season 2 – Peacemaker

Play trailer1:39Poster for Season 2 – PeacemakerNext Ep Thu Sep 11ComedyActionPlayTrailer Watchlist
WatchlistTomatometerPopcornmeter
99%Tomatometer82 Reviews 80%Popcornmeter1,000+ Ratings
A man fights for peace at any cost, no matter how many people he has to kill to get it.
Watch on MaxStream Now

Where to Watch

Peacemaker — Season 2

Peacemaker — Season 2

What to Know

Critics Consensus

Peacemaker's second season goes multidimensional while still maintaining a singular focus on emotional stakes, seamlessly transporting this outrageous antihero into a fresh cinematic universe.

Read Critics Reviews

Critics Reviews

View More (82)
Craig Mathieson The Age (Australia) Dimensional travel, vengeful relatives and spurts of bloody violence set the tone for a series that remains a cartoonish change of pace. Rated: 3/5Aug 28, 2025 Full ReviewGraeme Virtue Guardian Gunn clearly loved these damaged characters enough to carry them over to his shiny new universe, but he is also never afraid to put them through the wringer. It makes even the tiniest victories in their lives feel momentous. Rated: 4/5Aug 25, 2025 Full ReviewSam Adams Slate Like the first season, the second season of Peacemaker risks stretching too few jokes over too many hours—even if sometimes the joke going on for too long is the joke. But it’s also strangely fascinating. Aug 25, 2025 Full ReviewDavid Craig Radio Times From the opening dance number, it's an enormous amount of fun that carefully balances its surreal pleasures with impactful character-led moments – and plenty of unexpected twists. Rated: 4/5Aug 30, 2025 Full ReviewAlex Maidy JoBlo's Movie Network While I do not suffer from bird blindness, I can say that Peacemaker soars with the ducks. Rated: 9/10Aug 28, 2025 Full ReviewJoel Keller Decider Peacemaker continues to be a funny but emotional superhero drama, with a surprisingly effective performance by Cena at its center, with a fun-to-watch ensemble around him. Aug 25, 2025 Full ReviewRead all reviews

Audience Reviews

View More (330)
Andres G No es mala pero tampoco no es una de las mejores series como dice la critica Rated 3.5 out of 5 stars09/07/25Full Review Jude C Absolute-cinema, it's too bad we can't see peacemaker though Rated 5 out of 5 stars09/07/25Full Review Łukasz S Good Rated 5 out of 5 stars09/06/25Full Review Fernando N Essa segunda temporada está me surpreendendo positivamente, a química dos personagens estão incríveis, cenas de ação perfeitas e as participações surpreendentes Rated 5 out of 5 stars09/06/25Full Review Shan V starting to introduce old characters like theyre somehow important to the DCU, thge comedy puts me off and reminds me of the beginnings of Marvel shows where they started out good then went to crap. Rated 0.5 out of 5 stars09/07/25Full Review Zubair A With Gunn at the helm, the mission continues: flawless action, killer music, and jokes that hit like a helmet-beam. Fabulous doesn't even cover it. Rated 5 out of 5 stars09/05/25Full Review Read all reviews
Peacemaker — Season 2

My Rating

Read MoreRead LessPOST RATING WRITE A REVIEW EDIT REVIEW

Episodes

Episode 1Aired Aug 21, 2025The Ties That GrindWhile Peacemaker attempts to join the Justice Gang, Harcourt struggles to find work, and Economos takes on a challenging new assignment.Details Episode 2Aired Aug 28, 2025A Man Is Only as Good as His BirdAs Economos clashes with his new handler, Peacemaker must deal with the consequences of his actions in the alternate dimension.Details Episode 3Aired Sep 4, 2025Another Rick Up My SleeveA man fights for peace at any cost, no matter how many people he has to kill to get it.Details Episode 4Airs Thu Sep 11Need I Say DoorA man fights for peace at any cost, no matter how many people he has to kill to get it.Details Episode 5Airs Thu Sep 18Back to the SutureA man fights for peace at any cost, no matter how many people he has to kill to get it.Details Episode 6Airs Thu Sep 25Ignorance is ChrisA man fights for peace at any cost, no matter how many people he has to kill to get it.Details Episode 7Airs Thu Oct 2A man fights for peace at any cost, no matter how many people he has to kill to get it.Details Episode 8Airs Thu Oct 9A man fights for peace at any cost, no matter how many people he has to kill to get it.Details
Twisted Metal79% 88% Twisted Metal WatchlistTRAILERfor Twisted MetalComrade Detective85% 88% Comrade Detective WatchlistSaul of the Mole Men17% % Saul of the Mole Men WatchlistSomebody Somewhere100% 93% Somebody Somewhere WatchlistTRAILERfor Somebody SomewhereSouth Side100% 92% South Side WatchlistTRAILERfor South SideDiscover more movies and TV shows.View More

Season Info

Executive Producer
James Gunn, Peter Safran, John Cena
Screenwriter
James Gunn
Network
HBO Max
Rating
TV-MA
Genre
Comedy, Action
Original Language
English
Release Date
Aug 21, 2025