Skip to content

Commit ada593e

Browse files
authored
Merge pull request #106 from RealTeamRocket/104-map-display-for-past-runs
104 map display for past runs
2 parents 80c3338 + 401a14a commit ada593e

39 files changed

Lines changed: 1930 additions & 130 deletions

rocket-backend/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ require (
2020
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
2121
github.com/hashicorp/errwrap v1.1.0 // indirect
2222
github.com/hashicorp/go-multierror v1.1.1 // indirect
23-
github.com/lib/pq v1.10.9 // indirect
23+
github.com/lib/pq v1.10.9
2424
github.com/stretchr/testify v1.10.0 // indirect
2525
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
2626
go.uber.org/atomic v1.7.0 // indirect

rocket-backend/integration-tests/internal/challenges-tests/main_test.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@ import (
55
"database/sql"
66
"errors"
77
"fmt"
8-
"net/http"
98
"os"
109
"path/filepath"
11-
"rocket-backend/internal/database"
12-
"rocket-backend/internal/server"
1310
"rocket-backend/pkg/logger"
1411
"runtime"
1512
"testing"
@@ -46,30 +43,14 @@ type TestDatabase struct {
4643
}
4744

4845
var testDB *TestDatabase
49-
var testServer *http.Server
50-
var baseURL string
51-
var port = 8090
5246

5347
var _ = BeforeSuite(func() {
5448
testDB = SetupTestDatabase()
5549
testDbInstance = testDB.DbInstance
5650

57-
// Start API server for all tests in this package
58-
connStr := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", DbUser, DbPass, testDB.DbAddress, DbName)
59-
dbService := database.NewWithConfig(connStr)
60-
testServer = &http.Server{
61-
Addr: fmt.Sprintf(":%d", port),
62-
Handler: server.NewServerWithDB(dbService, port, "testsecret").RegisterRoutes(),
63-
}
64-
go testServer.ListenAndServe()
65-
time.Sleep(1 * time.Second)
66-
baseURL = fmt.Sprintf("http://localhost:%d/api/v1", port)
6751
})
6852

6953
var _ = AfterSuite(func() {
70-
if testServer != nil {
71-
testServer.Close()
72-
}
7354
testDB.TearDown()
7455
})
7556

rocket-backend/integration-tests/internal/database-tests/main_test.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@ import (
55
"database/sql"
66
"errors"
77
"fmt"
8-
"net/http"
98
"os"
109
"path/filepath"
11-
"rocket-backend/internal/database"
12-
"rocket-backend/internal/server"
1310
"rocket-backend/pkg/logger"
1411
"runtime"
1512
"testing"
@@ -46,30 +43,14 @@ type TestDatabase struct {
4643
}
4744

4845
var testDB *TestDatabase
49-
var testServer *http.Server
50-
var baseURL string
51-
var port = 8090
5246

5347
var _ = BeforeSuite(func() {
5448
testDB = SetupTestDatabase()
5549
testDbInstance = testDB.DbInstance
5650

57-
// Start API server for all tests in this package
58-
connStr := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", DbUser, DbPass, testDB.DbAddress, DbName)
59-
dbService := database.NewWithConfig(connStr)
60-
testServer = &http.Server{
61-
Addr: fmt.Sprintf(":%d", port),
62-
Handler: server.NewServerWithDB(dbService, port, "testsecret").RegisterRoutes(),
63-
}
64-
go testServer.ListenAndServe()
65-
time.Sleep(1 * time.Second)
66-
baseURL = fmt.Sprintf("http://localhost:%d/api/v1", port)
6751
})
6852

6953
var _ = AfterSuite(func() {
70-
if testServer != nil {
71-
testServer.Close()
72-
}
7354
testDB.TearDown()
7455
})
7556

rocket-backend/integration-tests/internal/database-tests/runs_table_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,127 @@ var _ = Describe("Runs Table Integration", func() {
111111
Expect(err.Error()).To(ContainSubstring("violates foreign key constraint"))
112112
})
113113
})
114+
115+
var _ = Describe("Planned Runs Table Integration", func() {
116+
var userID uuid.UUID
117+
118+
BeforeEach(func() {
119+
userID = uuid.New()
120+
now := time.Now()
121+
// Insert into credentials first (to satisfy users FK)
122+
_, err := testDbInstance.Exec(`
123+
INSERT INTO credentials (id, email, password, created_at, last_login)
124+
VALUES ($1, $2, $3, $4, $5)
125+
`, userID, "planuser@example.com", "hashedpassword", now, now)
126+
Expect(err).To(BeNil())
127+
128+
// Insert into users
129+
_, err = testDbInstance.Exec(`
130+
INSERT INTO users (id, username, email, rocketpoints)
131+
VALUES ($1, $2, $3, $4)
132+
`, userID, "planuser", "planuser@example.com", 0)
133+
Expect(err).To(BeNil())
134+
})
135+
136+
AfterEach(func() {
137+
testDbInstance.Exec("DELETE FROM planned_runs")
138+
testDbInstance.Exec("DELETE FROM users")
139+
testDbInstance.Exec("DELETE FROM credentials")
140+
})
141+
142+
It("should insert and retrieve a planned run for a user", func() {
143+
route := "LINESTRING(2 2,3 3)"
144+
name := "Morning Plan"
145+
distance := 5.0
146+
147+
_, err := testDbInstance.Exec(`
148+
INSERT INTO planned_runs (user_id, route, name, distance)
149+
VALUES ($1, ST_GeomFromText($2, 4326), $3, $4)
150+
`, userID, route, name, distance)
151+
Expect(err).To(BeNil())
152+
153+
rows, err := testDbInstance.Query(`
154+
SELECT id, ST_AsText(route), name, created_at, distance
155+
FROM planned_runs
156+
WHERE user_id = $1
157+
`, userID)
158+
Expect(err).To(BeNil())
159+
defer rows.Close()
160+
161+
var found bool
162+
for rows.Next() {
163+
var id uuid.UUID
164+
var gotRoute, gotName string
165+
var createdAt time.Time
166+
var gotDistance float64
167+
err := rows.Scan(&id, &gotRoute, &gotName, &createdAt, &gotDistance)
168+
Expect(err).To(BeNil())
169+
Expect(gotRoute).To(Equal(route))
170+
Expect(gotName).To(Equal(name))
171+
Expect(gotDistance).To(BeNumerically("~", distance, 0.01))
172+
found = true
173+
}
174+
Expect(found).To(BeTrue())
175+
})
176+
177+
It("should delete a planned run", func() {
178+
route := "LINESTRING(2 2,3 3)"
179+
name := "Delete Plan"
180+
distance := 3.0
181+
182+
var planID uuid.UUID
183+
err := testDbInstance.QueryRow(`
184+
INSERT INTO planned_runs (user_id, route, name, distance)
185+
VALUES ($1, ST_GeomFromText($2, 4326), $3, $4)
186+
RETURNING id
187+
`, userID, route, name, distance).Scan(&planID)
188+
Expect(err).To(BeNil())
189+
190+
_, err = testDbInstance.Exec(`
191+
DELETE FROM planned_runs WHERE id = $1
192+
`, planID)
193+
Expect(err).To(BeNil())
194+
195+
row := testDbInstance.QueryRow(`
196+
SELECT COUNT(*) FROM planned_runs WHERE id = $1
197+
`, planID)
198+
var count int
199+
err = row.Scan(&count)
200+
Expect(err).To(BeNil())
201+
Expect(count).To(Equal(0))
202+
})
203+
204+
It("should enforce unique constraint on (user_id, name)", func() {
205+
route := "LINESTRING(2 2,3 3)"
206+
name := "Unique Plan"
207+
distance := 4.0
208+
209+
_, err := testDbInstance.Exec(`
210+
INSERT INTO planned_runs (user_id, route, name, distance)
211+
VALUES ($1, ST_GeomFromText($2, 4326), $3, $4)
212+
`, userID, route, name, distance)
213+
Expect(err).To(BeNil())
214+
215+
// Try to insert another planned run with the same user_id and name
216+
_, err = testDbInstance.Exec(`
217+
INSERT INTO planned_runs (user_id, route, name, distance)
218+
VALUES ($1, ST_GeomFromText($2, 4326), $3, $4)
219+
`, userID, route, name, distance)
220+
Expect(err).ToNot(BeNil())
221+
Expect(err.Error()).To(ContainSubstring("duplicate key"))
222+
})
223+
224+
It("should enforce foreign key constraint on user_id", func() {
225+
nonExistentUserID := uuid.New()
226+
route := "LINESTRING(2 2,3 3)"
227+
name := "FK Plan"
228+
distance := 2.5
229+
230+
_, err := testDbInstance.Exec(`
231+
INSERT INTO planned_runs (user_id, route, name, distance)
232+
VALUES ($1, ST_GeomFromText($2, 4326), $3, $4)
233+
`, nonExistentUserID, route, name, distance)
234+
Expect(err).ToNot(BeNil())
235+
Expect(err.Error()).To(ContainSubstring("violates foreign key constraint"))
236+
})
237+
})

rocket-backend/integration-tests/internal/server-tests/main_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,23 @@ var _ = BeforeSuite(func() {
6262
Handler: server.NewServerWithDB(dbService, port, "testsecret").RegisterRoutes(),
6363
}
6464
go testServer.ListenAndServe()
65-
time.Sleep(1 * time.Second)
65+
66+
// Wait for the server to be ready
67+
ready := false
68+
for range 30 {
69+
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/api/v1/health", port))
70+
if err == nil && resp.StatusCode == 200 {
71+
ready = true
72+
resp.Body.Close()
73+
break
74+
}
75+
time.Sleep(100 * time.Millisecond)
76+
}
77+
if !ready {
78+
panic("server did not start in time")
79+
}
6680
baseURL = fmt.Sprintf("http://localhost:%d/api/v1", port)
81+
6782
})
6883

6984
var _ = AfterSuite(func() {

rocket-backend/integration-tests/internal/server-tests/run_routes_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,95 @@ var _ = Describe("Run Handlers API", func() {
8181
defer resp.Body.Close()
8282
Expect(resp.StatusCode).To(Equal(400))
8383
})
84+
85+
It("should create, list, and delete a planned run", func() {
86+
// Create a planned run
87+
planPayload := map[string]any{
88+
"route": "LINESTRING(2 2,3 3)",
89+
"name": "Morning Run",
90+
"distance": 5.0,
91+
}
92+
planBody, _ := json.Marshal(planPayload)
93+
req, _ := http.NewRequest("POST", baseURL+"/protected/runs/plan", bytes.NewReader(planBody))
94+
req.Header.Set("Authorization", "Bearer "+token)
95+
req.Header.Set("Content-Type", "application/json")
96+
resp, err := http.DefaultClient.Do(req)
97+
Expect(err).To(BeNil())
98+
defer resp.Body.Close()
99+
Expect(resp.StatusCode).To(Equal(200))
100+
101+
// List planned runs
102+
req, _ = http.NewRequest("GET", baseURL+"/protected/runs/plan", nil)
103+
req.Header.Set("Authorization", "Bearer "+token)
104+
resp, err = http.DefaultClient.Do(req)
105+
Expect(err).To(BeNil())
106+
defer resp.Body.Close()
107+
Expect(resp.StatusCode).To(Equal(200))
108+
var plannedRuns []map[string]any
109+
_ = json.NewDecoder(resp.Body).Decode(&plannedRuns)
110+
Expect(len(plannedRuns)).To(Equal(1))
111+
Expect(plannedRuns[0]["name"]).To(Equal("Morning Run"))
112+
Expect(plannedRuns[0]["distance"]).To(Equal(5.0))
113+
plannedRunID := plannedRuns[0]["id"].(string)
114+
115+
// Delete the planned run
116+
req, _ = http.NewRequest("DELETE", baseURL+"/protected/runs/plan/"+plannedRunID, nil)
117+
req.Header.Set("Authorization", "Bearer "+token)
118+
resp, err = http.DefaultClient.Do(req)
119+
Expect(err).To(BeNil())
120+
defer resp.Body.Close()
121+
Expect(resp.StatusCode).To(Equal(200))
122+
123+
// List planned runs again (should be empty)
124+
req, _ = http.NewRequest("GET", baseURL+"/protected/runs/plan", nil)
125+
req.Header.Set("Authorization", "Bearer "+token)
126+
resp, err = http.DefaultClient.Do(req)
127+
Expect(err).To(BeNil())
128+
defer resp.Body.Close()
129+
Expect(resp.StatusCode).To(Equal(200))
130+
_ = json.NewDecoder(resp.Body).Decode(&plannedRuns)
131+
Expect(len(plannedRuns)).To(Equal(0))
132+
})
133+
134+
It("should not allow duplicate planned run names", func() {
135+
planPayload := map[string]any{
136+
"route": "LINESTRING(4 4,5 5)",
137+
"name": "Duplicate Run",
138+
"distance": 7.0,
139+
}
140+
planBody, _ := json.Marshal(planPayload)
141+
req, _ := http.NewRequest("POST", baseURL+"/protected/runs/plan", bytes.NewReader(planBody))
142+
req.Header.Set("Authorization", "Bearer "+token)
143+
req.Header.Set("Content-Type", "application/json")
144+
resp, err := http.DefaultClient.Do(req)
145+
Expect(err).To(BeNil())
146+
defer resp.Body.Close()
147+
Expect(resp.StatusCode).To(Equal(200))
148+
149+
// Try to create another planned run with the same name
150+
req, _ = http.NewRequest("POST", baseURL+"/protected/runs/plan", bytes.NewReader(planBody))
151+
req.Header.Set("Authorization", "Bearer "+token)
152+
req.Header.Set("Content-Type", "application/json")
153+
resp, err = http.DefaultClient.Do(req)
154+
Expect(err).To(BeNil())
155+
defer resp.Body.Close()
156+
Expect(resp.StatusCode).To(Equal(400))
157+
})
158+
159+
It("should not allow unauthorized access to planned runs", func() {
160+
req, _ := http.NewRequest("GET", baseURL+"/protected/runs/plan", nil)
161+
resp, err := http.DefaultClient.Do(req)
162+
Expect(err).To(BeNil())
163+
defer resp.Body.Close()
164+
Expect(resp.StatusCode).To(Equal(401))
165+
})
166+
167+
It("should return error for invalid planned run ID on delete", func() {
168+
req, _ := http.NewRequest("DELETE", baseURL+"/protected/runs/plan/invalid-uuid", nil)
169+
req.Header.Set("Authorization", "Bearer "+token)
170+
resp, err := http.DefaultClient.Do(req)
171+
Expect(err).To(BeNil())
172+
defer resp.Body.Close()
173+
Expect(resp.StatusCode).To(Equal(400))
174+
})
84175
})

rocket-backend/internal/database/database.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ type Service interface {
8080
SaveRun(userID uuid.UUID, route string, duration string, distance float64) error
8181
GetAllRunsByUser(userID uuid.UUID) ([]types.RunDTO, error)
8282
DeleteRun(runID uuid.UUID) error
83+
SavePlannedRun(userID uuid.UUID, route string, name string, distance float64) error
84+
GetAllPlannedRunsByUser(userID uuid.UUID) ([]types.PlannedRunDTO, error)
85+
DeletePlannedRun(runID uuid.UUID) error
8386

8487
// activities
8588
SaveActivity(userID uuid.UUID, message string) error

0 commit comments

Comments
 (0)