From f397bee1ade0830ff89b3e2cd400f15bda5baedf Mon Sep 17 00:00:00 2001 From: Stepan Date: Fri, 6 Mar 2026 16:23:37 +0300 Subject: [PATCH] v1.0.0 --- .gitignore | 1 + go.mod | 2 +- main.go | 8 +++- parcel.go | 106 ++++++++++++++++++++++++++++++++++++++++++------- parcel_test.go | 77 +++++++++++++++++++++++++++++------ tracker.db | Bin 61440 -> 0 bytes 6 files changed, 165 insertions(+), 29 deletions(-) create mode 100644 .gitignore delete mode 100644 tracker.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..848bce1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tracker.db diff --git a/go.mod b/go.mod index 20eb2205..00733d9d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Yandex-Practicum/go-db-sql-final +module github.com/St-Ivanov/sprint8 go 1.21 diff --git a/main.go b/main.go index 44c32b3f..a73ece67 100644 --- a/main.go +++ b/main.go @@ -98,8 +98,12 @@ func (s ParcelService) Delete(number int) error { func main() { // настройте подключение к БД - - store := // создайте объект ParcelStore функцией NewParcelStore + db, err := sql.Open("sqlite", "tracker.db") + if err != nil { + return + } + defer db.Close() + store := NewParcelStore(db) // создайте объект ParcelStore функцией NewParcelStore service := NewParcelService(store) // регистрация посылки diff --git a/parcel.go b/parcel.go index db6c815d..ea970855 100644 --- a/parcel.go +++ b/parcel.go @@ -2,6 +2,12 @@ package main import ( "database/sql" + "errors" + "time" +) + +var ( + errStatusNoRegistered = errors.New("Попытка изменить объект. Статус нерегистрация.") ) type ParcelStore struct { @@ -14,47 +20,119 @@ func NewParcelStore(db *sql.DB) ParcelStore { func (s ParcelStore) Add(p Parcel) (int, error) { // реализуйте добавление строки в таблицу parcel, используйте данные из переменной p - + created_at := time.Now().UTC().Format(time.RFC3339) + data, err := s.db.Exec("INSERT INTO parcel (client, status, address, created_at) VALUES (:client, :status, :address, :created_at)", + sql.Named("client", p.Client), + sql.Named("status", p.Status), + sql.Named("address", p.Address), + sql.Named("created_at", created_at)) + if err != nil { + return 0, err + } + id, err := data.LastInsertId() // верните идентификатор последней добавленной записи - return 0, nil + if err != nil { + return 0, err + } + return int(id), nil } func (s ParcelStore) Get(number int) (Parcel, error) { // реализуйте чтение строки по заданному number // здесь из таблицы должна вернуться только одна строка - + row := s.db.QueryRow(` + SELECT number, client, status, address, created_at + FROM parcel + WHERE number = :number + `, + sql.Named("number", number), + ) // заполните объект Parcel данными из таблицы p := Parcel{} + err := row.Scan(&p.Number, &p.Client, &p.Status, &p.Address, &p.CreatedAt) - return p, nil + return p, err } func (s ParcelStore) GetByClient(client int) ([]Parcel, error) { // реализуйте чтение строк из таблицы parcel по заданному client // здесь из таблицы может вернуться несколько строк - - // заполните срез Parcel данными из таблицы var res []Parcel - - return res, nil + rows, err := s.db.Query(` + SELECT number, client, status, address, created_at + FROM parcel + WHERE client = :client + `, + sql.Named("client", client), + ) + // заполните срез Parcel данными из таблицы + if err != nil { + return res, err + } + defer rows.Close() + + for rows.Next() { + p := Parcel{} + + err := rows.Scan(&p.Number, &p.Client, &p.Status, &p.Address, &p.CreatedAt) + if err != nil { + return res, err + } + res = append(res, p) + } + err = rows.Err() + return res, err } func (s ParcelStore) SetStatus(number int, status string) error { // реализуйте обновление статуса в таблице parcel - - return nil + _, err := s.db.Exec(` + UPDATE parcel + SET status = :status + WHERE number = :number + `, + sql.Named("status", status), + sql.Named("number", number), + ) + return err } func (s ParcelStore) SetAddress(number int, address string) error { // реализуйте обновление адреса в таблице parcel // менять адрес можно только если значение статуса registered - - return nil + p, err := s.Get(number) + if err != nil { + return err + } + if p.Address == ParcelStatusSent || p.Address == ParcelStatusDelivered { + return errStatusNoRegistered + } + _, err = s.db.Exec(` + UPDATE parcel + SET address = :address + WHERE number = :number + `, + sql.Named("address", address), + sql.Named("number", number), + ) + return err } func (s ParcelStore) Delete(number int) error { // реализуйте удаление строки из таблицы parcel // удалять строку можно только если значение статуса registered - - return nil + p, err := s.Get(number) + if err != nil { + return err + } + if p.Address == ParcelStatusSent || p.Address == ParcelStatusDelivered { + return errStatusNoRegistered + } + _, err = s.db.Exec(` + DELETE FROM parcel + WHERE number = :number + `, + sql.Named("number", number), + ) + return err } diff --git a/parcel_test.go b/parcel_test.go index d1b93827..08f1f164 100644 --- a/parcel_test.go +++ b/parcel_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -31,57 +32,100 @@ func getTestParcel() Parcel { // TestAddGetDelete проверяет добавление, получение и удаление посылки func TestAddGetDelete(t *testing.T) { // prepare - db, err := // настройте подключение к БД + db, err := sql.Open("sqlite", "tracker.db") + require.NoError(t, err) + defer db.Close() + store := NewParcelStore(db) parcel := getTestParcel() // add // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - + id, err := store.Add(parcel) + parcel.Number = id + require.NoError(t, err) + assert.NotEqual(t, 0, id) // get // получите только что добавленную посылку, убедитесь в отсутствии ошибки // проверьте, что значения всех полей в полученном объекте совпадают со значениями полей в переменной parcel - + p, err := store.Get(id) + require.NoError(t, err) + assert.Equal(t, parcel, p) // delete // удалите добавленную посылку, убедитесь в отсутствии ошибки // проверьте, что посылку больше нельзя получить из БД + err = store.Delete(id) + require.NotErrorIs(t, err, errStatusNoRegistered) + + p, err = store.Get(id) + require.ErrorIs(t, err, sql.ErrNoRows) } // TestSetAddress проверяет обновление адреса func TestSetAddress(t *testing.T) { // prepare - db, err := // настройте подключение к БД + db, err := sql.Open("sqlite", "tracker.db") + require.NoError(t, err) + defer db.Close() + + store := NewParcelStore(db) + parcel := getTestParcel() // add // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - + id, err := store.Add(parcel) + require.NoError(t, err) + assert.NotEmpty(t, id) // set address // обновите адрес, убедитесь в отсутствии ошибки newAddress := "new test address" + err = store.SetAddress(id, newAddress) + + require.NoError(t, err) + // check // получите добавленную посылку и убедитесь, что адрес обновился + p, err := store.Get(id) + require.NoError(t, err) + assert.Equal(t, newAddress, p.Address) } // TestSetStatus проверяет обновление статуса func TestSetStatus(t *testing.T) { // prepare - db, err := // настройте подключение к БД + db, err := sql.Open("sqlite", "tracker.db") + require.NoError(t, err) + defer db.Close() + store := NewParcelStore(db) + parcel := getTestParcel() // add // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - + id, err := store.Add(parcel) + require.NoError(t, err) + assert.NotEmpty(t, id) // set status // обновите статус, убедитесь в отсутствии ошибки - + err = store.SetStatus(id, ParcelStatusSent) + require.NoError(t, err) // check // получите добавленную посылку и убедитесь, что статус обновился + p, err := store.Get(id) + require.NoError(t, err) + assert.Equal(t, ParcelStatusSent, p.Status) + } // TestGetByClient проверяет получение посылок по идентификатору клиента func TestGetByClient(t *testing.T) { // prepare - db, err := // настройте подключение к БД + db, err := sql.Open("sqlite", "tracker.db") + require.NoError(t, err) + defer db.Close() + + store := NewParcelStore(db) + parcel := getTestParcel() parcels := []Parcel{ getTestParcel(), @@ -98,8 +142,9 @@ func TestGetByClient(t *testing.T) { // add for i := 0; i < len(parcels); i++ { - id, err := // добавьте новую посылку в БД, убедитесь в отсутствии ошибки и наличии идентификатора - + id, err := store.Add(parcel) + require.NoError(t, err) + assert.NotEmpty(t, id) // обновляем идентификатор добавленной у посылки parcels[i].Number = id @@ -108,12 +153,20 @@ func TestGetByClient(t *testing.T) { } // get by client - storedParcels, err := // получите список посылок по идентификатору клиента, сохранённого в переменной client + storedParcels, err := store.GetByClient(client) // получите список посылок по идентификатору клиента, сохранённого в переменной client + require.NoError(t, err) // убедитесь в отсутствии ошибки // убедитесь, что количество полученных посылок совпадает с количеством добавленных // check for _, parcel := range storedParcels { + _, ok := parcelMap[parcel.Number] + assert.True(t, ok) + assert.NotEmpty(t, parcel.Number) + assert.NotEmpty(t, parcel.Client) + assert.NotEmpty(t, parcel.Status) + assert.NotEmpty(t, parcel.Address) + assert.NotEmpty(t, parcel.CreatedAt) // в parcelMap лежат добавленные посылки, ключ - идентификатор посылки, значение - сама посылка // убедитесь, что все посылки из storedParcels есть в parcelMap // убедитесь, что значения полей полученных посылок заполнены верно diff --git a/tracker.db b/tracker.db deleted file mode 100644 index b6ba48a148daa7c8c4727d4bfc1c868229d7fb24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61440 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