Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cmd/notes/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/rd2w/go-notes/internal/logger"
"github.com/rd2w/go-notes/internal/model"
"github.com/rd2w/go-notes/internal/repository"
"github.com/rd2w/go-notes/internal/repository/storage/fs"
"github.com/rd2w/go-notes/internal/repository/storage/ram"
"github.com/rd2w/go-notes/internal/service"
)

Expand All @@ -30,7 +32,7 @@ const (
AppShutdownMsg = "Приложение \"Заметки\" успешно завершило выполнение программы!"
ShutdownStartMsg = "Получен сигнал завершения, инициируем graceful shutdown..."
ResultsHeader = "\n=== РЕЗУЛЬТАТЫ ===\n"
NoteCountMsg = "Всего заметок создано: %d\n"
NoteCountMsg = "Всего заметок в хранилище данных: %d\n"
NoteDoesNotExistMsg = "Ошибка: заметка не существует"
NoteHeaderMsg = "Заметка %d:\n"
NoteIDMsg = " ID: %s\n"
Expand All @@ -50,8 +52,12 @@ func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)

// Явно регистрируем реализации
repository.Register(repository.RAM, ram.NewRamRepository)
repository.Register(repository.JSON, fs.NewJSONRepository)

// Инициализируем компоненты
repo := repository.NewRepository()
repo := repository.NewRepositoryByType(repository.JSON)
svc := service.NewService(repo, ctx, DataGenInterval)
newLogger := logger.NewLogger(repo, ctx, LoggerInterval)

Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/rd2w/go-notes

go 1.25.3

require github.com/stretchr/testify v1.11.1
require (
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.11.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
4 changes: 2 additions & 2 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import (

// Logger отвечает за логирование изменений в данных
type Logger struct {
repo *repository.Repository
repo repository.Repository
ctx context.Context
interval time.Duration
}

// NewLogger создает новый экземпляр логгера
func NewLogger(repo *repository.Repository, ctx context.Context, interval time.Duration) *Logger {
func NewLogger(repo repository.Repository, ctx context.Context, interval time.Duration) *Logger {
return &Logger{
repo: repo,
ctx: ctx,
Expand Down
22 changes: 11 additions & 11 deletions internal/logger/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"time"

"github.com/rd2w/go-notes/internal/model"
"github.com/rd2w/go-notes/internal/repository"
"github.com/rd2w/go-notes/internal/repository/storage/ram"
"github.com/rd2w/go-notes/internal/service"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -42,7 +42,7 @@ func TestLogger_IntegrationWithService(t *testing.T) {
// Создаем компоненты как в main()
ctx, cancel := context.WithCancel(context.Background())

repo := repository.NewRepository()
repo := ram.NewRamRepository()
svc := service.NewService(repo, ctx, 50*time.Millisecond)
logger := NewLogger(repo, ctx, 30*time.Millisecond)

Expand Down Expand Up @@ -81,7 +81,7 @@ func TestLogger_StopWithContext(t *testing.T) {

ctx, cancel := context.WithCancel(context.Background())

repo := repository.NewRepository()
repo := ram.NewRamRepository()

// Увеличиваем интервал логгера чтобы он реже проверял
logger := NewLogger(repo, ctx, 100*time.Millisecond)
Expand Down Expand Up @@ -128,7 +128,7 @@ func TestLogger_MultipleNoteGeneration(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

repo := repository.NewRepository()
repo := ram.NewRamRepository()
logger := NewLogger(repo, ctx, 40*time.Millisecond)

// Запускаем компоненты
Expand Down Expand Up @@ -189,7 +189,7 @@ func TestLogger_NoNotesScenario(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

repo := repository.NewRepository()
repo := ram.NewRamRepository()
logger := NewLogger(repo, ctx, 30*time.Millisecond)

// Запускаем только логгер, но не отправляем заметки
Expand All @@ -214,7 +214,7 @@ func TestLogger_ConcurrentAccess(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

repo := repository.NewRepository()
repo := ram.NewRamRepository()
// Увеличиваем интервал для стабильности
logger := NewLogger(repo, ctx, 30*time.Millisecond)

Expand Down Expand Up @@ -263,7 +263,7 @@ func TestLogger_TimeFormatConsistency(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

repo := repository.NewRepository()
repo := ram.NewRamRepository()
logger := NewLogger(repo, ctx, 50*time.Millisecond)

go logger.Start()
Expand Down Expand Up @@ -304,7 +304,7 @@ func TestLogger_SimpleCase(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

repo := repository.NewRepository()
repo := ram.NewRamRepository()
// Очень короткий интервал для быстрого обнаружения
logger := NewLogger(repo, ctx, 10*time.Millisecond)

Expand Down Expand Up @@ -341,7 +341,7 @@ func TestLogger_SeesNotesBeforeStop(t *testing.T) {

ctx, cancel := context.WithCancel(context.Background())

repo := repository.NewRepository()
repo := ram.NewRamRepository()

// Очень короткий интервал для быстрого обнаружения
logger := NewLogger(repo, ctx, 10*time.Millisecond)
Expand Down Expand Up @@ -387,7 +387,7 @@ func TestLogger_ImmediateStop(t *testing.T) {

ctx, cancel := context.WithCancel(context.Background())

repo := repository.NewRepository()
repo := ram.NewRamRepository()
logger := NewLogger(repo, ctx, 10*time.Millisecond)

// Останавливаем СРАЗУ ЖЕ
Expand Down Expand Up @@ -420,7 +420,7 @@ func TestLogger_GracefulStop(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // На этот раз используем defer

repo := repository.NewRepository()
repo := ram.NewRamRepository()

// Нормальный интервал
logger := NewLogger(repo, ctx, 50*time.Millisecond)
Expand Down
32 changes: 31 additions & 1 deletion internal/model/common.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package model

import "time"
import (
"encoding/json"
"time"
)

// TimeFields содержит общие временные метки для сущностей
type TimeFields struct {
Expand Down Expand Up @@ -29,3 +32,30 @@ func (t *TimeFields) initializeTimestamps() {
t.createdAt = now
t.updatedAt = now
}

// JSONTimeFields вспомогательная структура для JSON сериализации
type JSONTimeFields struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// MarshalJSON реализует интерфейс json.Marshaler
func (t *TimeFields) MarshalJSON() ([]byte, error) {
return json.Marshal(JSONTimeFields{
CreatedAt: t.createdAt,
UpdatedAt: t.updatedAt,
})
}

// UnmarshalJSON реализует интерфейс json.Unmarshaler
func (t *TimeFields) UnmarshalJSON(data []byte) error {
var jsonTimeFields JSONTimeFields
if err := json.Unmarshal(data, &jsonTimeFields); err != nil {
return err
}

t.createdAt = jsonTimeFields.CreatedAt
t.updatedAt = jsonTimeFields.UpdatedAt

return nil
}
47 changes: 40 additions & 7 deletions internal/model/note.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package model

import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"time"

"github.com/google/uuid"
)

// Note представляет сущность заметки
Expand Down Expand Up @@ -56,10 +58,41 @@ func (n *Note) SetContent(newContent string) {

// generateID генерирует уникальный идентификатор
func generateID() string {
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
panic("не удалось сгенерировать ID")
return uuid.New().String()
}

// JSONNote вспомогательная структура для JSON сериализации
type JSONNote struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// MarshalJSON реализует интерфейс json.Marshaler
func (n *Note) MarshalJSON() ([]byte, error) {
return json.Marshal(JSONNote{
ID: n.id,
Title: n.title,
Content: n.content,
CreatedAt: n.createdAt,
UpdatedAt: n.updatedAt,
})
}

// UnmarshalJSON реализует интерфейс json.Unmarshaler
func (n *Note) UnmarshalJSON(data []byte) error {
var jsonNote JSONNote
if err := json.Unmarshal(data, &jsonNote); err != nil {
return err
}
return base64.URLEncoding.EncodeToString(b)

n.id = jsonNote.ID
n.title = jsonNote.Title
n.content = jsonNote.Content
n.createdAt = jsonNote.CreatedAt
n.updatedAt = jsonNote.UpdatedAt

return nil
}
67 changes: 52 additions & 15 deletions internal/model/note_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package model

import (
"encoding/base64"
"fmt"
"testing"
"time"
Expand Down Expand Up @@ -261,11 +260,31 @@ func TestNoteIDGeneration(t *testing.T) {
t.Error("All note IDs should be unique")
}

// Проверяем формат ID (base64 URL encoding)
// Проверяем формат UUID
id := note1.GetID()
_, err := base64.URLEncoding.DecodeString(id)
if err != nil {
t.Errorf("Note ID should be valid base64 URL encoding: %v", err)

// Проверяем длину UUID
if len(id) != 36 {
t.Errorf("Note ID should be 36 characters long, got %d", len(id))
}

// Проверяем, что UUID имеет 4 дефиса на правильных позициях
dashes := []int{}
for i, char := range id {
if char == '-' {
dashes = append(dashes, i)
}
}

expectedDashes := []int{8, 13, 18, 23}
if len(dashes) != 4 {
t.Errorf("Expected 4 dashes in UUID, got %d", len(dashes))
} else {
for i, expectedPos := range expectedDashes {
if dashes[i] != expectedPos {
t.Errorf("Expected dash at position %d, got at position %d", expectedPos, dashes[i])
}
}
}
}

Expand Down Expand Up @@ -318,22 +337,40 @@ func TestNoteIDLengthAndFormat(t *testing.T) {
note := NewNote("Test", "Content")
id := note.GetID()

// Проверяем длину ID (base64 от 8 байт = 11 символов без padding или 12 с padding)
if len(id) != 11 && len(id) != 12 {
t.Errorf("Expected ID length 11 or 12, got %d for ID %q", len(id), id)
// Проверяем длину ID (UUID в формате string имеет длину 36 символов)
if len(id) != 36 {
t.Errorf("Expected ID length 36, got %d for ID %q", len(id), id)
}

// Проверяем что ID состоит из валидных base64 URL символов
isValidBase64URLChar := func(char rune) bool {
return (char >= 'A' && char <= 'Z') ||
(char >= 'a' && char <= 'z') ||
// Проверяем формат UUID
isValidUUIDChar := func(char rune) bool {
return (char >= 'a' && char <= 'f') ||
(char >= '0' && char <= '9') ||
char == '-' || char == '_' || char == '='
char == '-'
}

for _, char := range id {
if !isValidBase64URLChar(char) {
t.Errorf("ID contains invalid base64 URL character: %c", char)
if !isValidUUIDChar(char) {
t.Errorf("ID contains invalid UUID character: %c", char)
}
}

// Проверяем, что UUID имеет 4 дефиса на правильных позициях
dashes := []int{}
for i, char := range id {
if char == '-' {
dashes = append(dashes, i)
}
}

expectedDashes := []int{8, 13, 18, 23}
if len(dashes) != 4 {
t.Errorf("Expected 4 dashes in UUID, got %d", len(dashes))
} else {
for i, expectedPos := range expectedDashes {
if dashes[i] != expectedPos {
t.Errorf("Expected dash at position %d, got at position %d", expectedPos, dashes[i])
}
}
}
}
Expand Down
Loading