From 6dad788f1dbfaac95c6fe7adbb9f160b42975202 Mon Sep 17 00:00:00 2001 From: Maksim Krutovertsev Date: Tue, 28 Oct 2025 22:51:02 +0300 Subject: [PATCH] feat: add basic note entity structure and tests --- cmd/notes/main.go | 58 +++++++++ internal/model/common.go | 31 +++++ internal/model/note.go | 40 +++++++ internal/model/note_test.go | 231 ++++++++++++++++++++++++++++++++++++ 4 files changed, 360 insertions(+) create mode 100644 cmd/notes/main.go create mode 100644 internal/model/common.go create mode 100644 internal/model/note.go create mode 100644 internal/model/note_test.go diff --git a/cmd/notes/main.go b/cmd/notes/main.go new file mode 100644 index 0000000..afc3c27 --- /dev/null +++ b/cmd/notes/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "log" + "time" + + "github.com/rd2w/go-notes/internal/model" +) + +func main() { + note := model.NewNote("Первая заметка", "Это содержимое моей первой заметки") + displayNoteInfo("Исходная заметка:", note) + updateNote(note, "Обновленный заголовок", "Обновленное содержимое") + displayNoteInfo("После обновления:", note) + + log.Println("Приложение \"Заметки\" успешно завершило выполнение программы!") +} + +// displayNoteInfo отображает информацию о заметке в форматированном виде +func displayNoteInfo(header string, note *model.Note) { + if note == nil { + fmt.Println("Ошибка: заметка не существует") + return + } + + fmt.Println(header) + fmt.Printf(" Заголовок: %s\n", note.GetTitle()) + fmt.Printf(" Содержимое: %s\n", note.GetContent()) + fmt.Printf(" Создана: %s\n", formatTime(note.GetCreatedAt())) + fmt.Printf(" Обновлена: %s\n", formatTime(note.GetUpdatedAt())) + fmt.Println() +} + +// updateNote обновляет заголовок и содержимое заметки +func updateNote(note *model.Note, newTitle, newContent string) { + if note == nil { + fmt.Println("Ошибка: нельзя обновить несуществующую заметку!") + return + } + + fmt.Println("Обновление заметки...") + + if newTitle != "" { + note.SetTitle(newTitle) + } + + if newContent != "" { + note.SetContent(newContent) + } + + fmt.Printf("✅ Заметка успешно обновлена\n\n") +} + +// formatTime форматирует время в едином стиле +func formatTime(t time.Time) string { + return t.Format("2006-01-02 15:04:05") +} diff --git a/internal/model/common.go b/internal/model/common.go new file mode 100644 index 0000000..f0c4391 --- /dev/null +++ b/internal/model/common.go @@ -0,0 +1,31 @@ +package model + +import "time" + +// TimeFields содержит общие временные метки для сущностей +type TimeFields struct { + createdAt time.Time + updatedAt time.Time +} + +// GetCreatedAt возвращает время создания +func (t *TimeFields) GetCreatedAt() time.Time { + return t.createdAt +} + +// GetUpdatedAt возвращает время обновления +func (t *TimeFields) GetUpdatedAt() time.Time { + return t.updatedAt +} + +// updateTimestamp обновляет время изменения +func (t *TimeFields) updateTimestamp() { + t.updatedAt = time.Now() +} + +// initializeTimestamps инициализирует временные метки +func (t *TimeFields) initializeTimestamps() { + now := time.Now() + t.createdAt = now + t.updatedAt = now +} diff --git a/internal/model/note.go b/internal/model/note.go new file mode 100644 index 0000000..3c7af2c --- /dev/null +++ b/internal/model/note.go @@ -0,0 +1,40 @@ +package model + +// Note представляет сущность заметки +type Note struct { + TimeFields + title string + content string +} + +// NewNote создает новую заметку с инициализацией временных меток +func NewNote(title, content string) *Note { + note := &Note{ + title: title, + content: content, + } + note.initializeTimestamps() + return note +} + +// GetTitle возвращает заголовок заметки +func (n *Note) GetTitle() string { + return n.title +} + +// GetContent возвращает содержимое заметки +func (n *Note) GetContent() string { + return n.content +} + +// SetTitle устанавливает новый заголовок и обновляет временную метку +func (n *Note) SetTitle(newTitle string) { + n.title = newTitle + n.updateTimestamp() +} + +// SetContent устанавливает новое содержимое и обновляет временную метку +func (n *Note) SetContent(newContent string) { + n.content = newContent + n.updateTimestamp() +} diff --git a/internal/model/note_test.go b/internal/model/note_test.go new file mode 100644 index 0000000..6e379e8 --- /dev/null +++ b/internal/model/note_test.go @@ -0,0 +1,231 @@ +package model + +import ( + "testing" + "time" +) + +func TestNewNote(t *testing.T) { + title := "Test Title" + content := "Test Content" + + note := NewNote(title, content) + + if note == nil { + t.Fatal("NewNote returned nil") + } + + // Проверяем установку полей + if note.GetTitle() != title { + t.Errorf("Expected title %q, got %q", title, note.GetTitle()) + } + + if note.GetContent() != content { + t.Errorf("Expected content %q, got %q", content, note.GetContent()) + } + + // Проверяем инициализацию временных меток + if note.GetCreatedAt().IsZero() { + t.Error("CreatedAt should be initialized") + } + + if note.GetUpdatedAt().IsZero() { + t.Error("UpdatedAt should be initialized") + } + + // Проверяем, что created и updated равны при создании + if !note.GetCreatedAt().Equal(note.GetUpdatedAt()) { + t.Error("CreatedAt and UpdatedAt should be equal for new note") + } + + // Проверяем, что временные метки близки к текущему времени + now := time.Now() + createdAt := note.GetCreatedAt() + + if createdAt.After(now) { + t.Error("CreatedAt should not be in the future") + } + + // Допускаем небольшую погрешность во времени выполнения + if now.Sub(createdAt) > time.Second { + t.Error("CreatedAt should be close to current time") + } +} + +func TestNoteSetters(t *testing.T) { + note := NewNote("Initial Title", "Initial Content") + initialCreatedAt := note.GetCreatedAt() + initialUpdatedAt := note.GetUpdatedAt() + + // Даем небольшое время для обеспечения разных временных меток + time.Sleep(10 * time.Millisecond) + + // Тестируем SetTitle + newTitle := "Updated Title" + note.SetTitle(newTitle) + + if note.GetTitle() != newTitle { + t.Errorf("Expected title %q after SetTitle, got %q", newTitle, note.GetTitle()) + } + + // Проверяем, что updatedAt изменился + if note.GetUpdatedAt().Equal(initialUpdatedAt) { + t.Error("UpdatedAt should change after SetTitle") + } + + // Проверяем, что createdAt не изменился + if !note.GetCreatedAt().Equal(initialCreatedAt) { + t.Error("CreatedAt should not change after SetTitle") + } + + // Проверяем, что updatedAt стал позже + if note.GetUpdatedAt().Before(initialUpdatedAt) { + t.Error("UpdatedAt should be after previous UpdatedAt") + } + + // Сохраняем updatedAt после первого изменения + updatedAfterTitle := note.GetUpdatedAt() + time.Sleep(10 * time.Millisecond) + + // Тестируем SetContent + newContent := "Updated Content" + note.SetContent(newContent) + + if note.GetContent() != newContent { + t.Errorf("Expected content %q after SetContent, got %q", newContent, note.GetContent()) + } + + // Проверяем, что updatedAt снова изменился + if note.GetUpdatedAt().Equal(updatedAfterTitle) { + t.Error("UpdatedAt should change after SetContent") + } + + // Проверяем, что createdAt все еще не изменился + if !note.GetCreatedAt().Equal(initialCreatedAt) { + t.Error("CreatedAt should not change after SetContent") + } +} + +func TestNoteGetters(t *testing.T) { + title := "Getter Test Title" + content := "Getter Test Content" + + note := NewNote(title, content) + + // Тестируем геттеры + if got := note.GetTitle(); got != title { + t.Errorf("GetTitle() = %q, want %q", got, title) + } + + if got := note.GetContent(); got != content { + t.Errorf("GetContent() = %q, want %q", got, content) + } + + // Проверяем, что геттеры временных меток возвращают не-zero значения + if note.GetCreatedAt().IsZero() { + t.Error("GetCreatedAt() returned zero time") + } + + if note.GetUpdatedAt().IsZero() { + t.Error("GetUpdatedAt() returned zero time") + } +} + +func TestTimeFieldsMethods(t *testing.T) { + tf := &TimeFields{} + + // До инициализации временные метки должны быть zero + if !tf.GetCreatedAt().IsZero() { + t.Error("CreatedAt should be zero before initialization") + } + + if !tf.GetUpdatedAt().IsZero() { + t.Error("UpdatedAt should be zero before initialization") + } + + // Инициализируем временные метки + tf.initializeTimestamps() + + // После инициализации не должны быть zero + if tf.GetCreatedAt().IsZero() { + t.Error("CreatedAt should not be zero after initialization") + } + + if tf.GetUpdatedAt().IsZero() { + t.Error("UpdatedAt should not be zero after initialization") + } + + // Сохраняем текущие значения + initialCreatedAt := tf.GetCreatedAt() + initialUpdatedAt := tf.GetUpdatedAt() + + time.Sleep(10 * time.Millisecond) + + // Обновляем временную метку + tf.updateTimestamp() + + // Проверяем, что updatedAt изменился, а createdAt остался прежним + if tf.GetCreatedAt() != initialCreatedAt { + t.Error("CreatedAt should not change after updateTimestamp") + } + + if tf.GetUpdatedAt().Equal(initialUpdatedAt) { + t.Error("UpdatedAt should change after updateTimestamp") + } + + if tf.GetUpdatedAt().Before(initialUpdatedAt) { + t.Error("UpdatedAt should be after previous value") + } +} + +func TestNoteEdgeCases(t *testing.T) { + // Тестируем пустые значения + emptyNote := NewNote("", "") + if emptyNote.GetTitle() != "" { + t.Error("Should handle empty title") + } + if emptyNote.GetContent() != "" { + t.Error("Should handle empty content") + } + + // Тестируем установку пустых значений + note := NewNote("Title", "Content") + note.SetTitle("") + if note.GetTitle() != "" { + t.Error("SetTitle should handle empty string") + } + + note.SetContent("") + if note.GetContent() != "" { + t.Error("SetContent should handle empty string") + } +} + +func TestMultipleNotes(t *testing.T) { + // Создаем несколько заметок и проверяем их независимость + note1 := NewNote("Note 1", "Content 1") + note2 := NewNote("Note 2", "Content 2") + + // Проверяем, что у них разные временные метки + // (могут быть равны если созданы в одну наносекунду, но это маловероятно) + if note1.GetCreatedAt().Equal(note2.GetCreatedAt()) { + t.Log("Note: Both notes have same CreatedAt (very close creation time)") + } + + // Проверяем независимость данных + if note1.GetTitle() == note2.GetTitle() { + t.Error("Notes should have different titles") + } + + if note1.GetContent() == note2.GetContent() { + t.Error("Notes should have different content") + } + + // Изменяем одну заметку и проверяем, что другая не изменилась + note1OriginalTitle := note1.GetTitle() + note2.SetTitle("Modified Note 2") + + if note1.GetTitle() != note1OriginalTitle { + t.Error("Modifying one note should not affect another") + } +}