Skip to content

Commit d292e95

Browse files
feat: add cases for different datetime values (#164)
* chore: replicate mysql datetime invalid issue Signed-off-by: Akash Kumar <meakash7902@gmail.com> * docs: add commands to replicate invalid datetime Signed-off-by: Akash Kumar <meakash7902@gmail.com> * fix: change name of createdBy field Signed-off-by: Akash Kumar <meakash7902@gmail.com> * feat: add cases for datetime validation Signed-off-by: Akash Kumar <meakash7902@gmail.com> * docs: add datetime endpoints curl commands Signed-off-by: Akash Kumar <meakash7902@gmail.com> * fix: remove non-existent curl command from readme and fix lint Signed-off-by: Akash Kumar <meakash7902@gmail.com> * fix: linting issues due to improper comments Signed-off-by: Akash Kumar <meakash7902@gmail.com> --------- Signed-off-by: Akash Kumar <meakash7902@gmail.com>
1 parent ee97b34 commit d292e95

4 files changed

Lines changed: 310 additions & 34 deletions

File tree

echo-mysql/README.md

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,68 @@ sudo -E env PATH=$PATH oss record -c "./echo-mysql"
3939
To generate testcases we just need to make some API calls. You can use Postman, Hoppscotch, or simply curl
4040

4141

42-
1. Root Endpoint:
42+
### 1) Basics
43+
4344
```bash
44-
-> curl -X GET http://localhost:9090/
45+
# health
46+
curl -X GET http://localhost:9090/
47+
48+
# hello
49+
curl -X GET http://localhost:9090/healthcheck
50+
51+
# create a short url
52+
curl -X POST http://localhost:9090/shorten -H "Content-Type: application/json" -d '{"url": "https://github.com"}'
53+
54+
# resolve a short code
55+
curl -X GET http://localhost:9090/resolve/4KepjkTT
4556
```
4657

58+
### 2) Seed (single)
59+
60+
Seeds a single record with a far-future end time.
4761

48-
2. Health Check:
4962
```bash
50-
-> curl -X GET http://localhost:9090/healthcheck
63+
curl -i -X POST http://localhost:9090/seed
5164
```
5265

66+
### 3) Seed datetime edge-cases
67+
68+
This **inserts a suite of tricky date/time rows** to help catch regressions:
69+
70+
* `dt-sentinel-9999-01-01T00:00:00Z` → end\_time `9999-01-01`
71+
* `dt-max-9999-12-31T23:59:59.999999Z` → end\_time `9999-12-31 23:59:59.999999`
72+
* `dt-min-1000-01-01T00:00:00Z` → end\_time `1000-01-01`
73+
* `dt-epoch-1970-01-01T00:00:00Z` → end\_time `1970-01-01`
74+
* `dt-leap-2020-02-29T12:34:56Z` → end\_time `2020-02-29 12:34:56`
75+
* `dt-offset-2023-07-01T18:30:00+05:30` → end\_time normalized to local (e.g., `2023-07-01 13:00:00` if `loc=Local`)
76+
* `dt-now-trunc` → end\_time is “now”, truncated to microseconds
77+
78+
Each is tagged with `created_by = "keploy.io/dates"`.
5379

54-
3. Short URL:
5580
```bash
56-
-> curl -X POST http://localhost:9090/shorten -H "Content-Type: application/json" -d '{"url": "https://github.com"}'
81+
curl -i -X POST http://localhost:9090/seed/dates
5782
```
5883

84+
### 4) Query helpers for replay validation
5985

60-
4. Resolve short code:
86+
# Find rows with end_time exactly equal to the given timestamp (ISO-8601 accepted)
87+
# e.g., date-only, full datetime, fractional seconds, or 'Z'
88+
curl -i "http://localhost:9090/query/by-endtime?ts=9999-12-31T23:59:59.999999Z"
89+
curl -i "http://localhost:9090/query/by-endtime?ts=9999-01-01T00:00:00Z"
6190

62-
```bash
63-
-> curl -X GET http://localhost:9090/resolve/4KepjkTT
91+
# Return just the two sentinel extremes (min/max)
92+
curl -i http://localhost:9090/query/sentinels
93+
94+
# Get a seeded row by 'short_code' label
95+
curl -i http://localhost:9090/query/label/dt-leap-2020-02-29T12:34:56Z
96+
97+
# List ONLY the seeded date cases (created_by = keploy.io/dates)
98+
curl -i http://localhost:9090/query/dates
6499
```
65100
101+
> These queries are designed so Keploy can record exact MySQL traffic and later replay it to **catch subtle encoder/decoder regressions** in date/time handling.
102+
103+
66104
Now both these API calls were captured as a testcase and should be visible on the Keploy CLI. You should be seeing an app named keploy folder with the test cases we just captured and data mocks created.
67105
68106
![alt text](https://github.com/Hermione2408/samples-go/blob/app/echo-mysql/img/keploy_record.png?raw=true)

echo-mysql/main.go

Lines changed: 144 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// Package main initializes and starts a URL shortening service using Echo framework,
2-
// connecting to a MySQL database and providing endpoints for shortening and resolving URLs
1+
// Package main implements the HTTP server for the URL shortening service.
32
package main
43

54
import (
@@ -15,13 +14,32 @@ import (
1514
"github.com/labstack/echo/middleware"
1615
)
1716

17+
// ensure UTC in JSON responses for determinism across environments
18+
func utcInfo(in *uss.ShortCodeInfo) *uss.ShortCodeInfo {
19+
if in == nil {
20+
return nil
21+
}
22+
cp := *in
23+
cp.EndTime = cp.EndTime.UTC()
24+
cp.UpdatedAt = cp.UpdatedAt.UTC()
25+
return &cp
26+
}
27+
func utcInfos(in []uss.ShortCodeInfo) []uss.ShortCodeInfo {
28+
out := make([]uss.ShortCodeInfo, len(in))
29+
for i := range in {
30+
out[i] = in[i]
31+
out[i].EndTime = in[i].EndTime.UTC()
32+
out[i].UpdatedAt = in[i].UpdatedAt.UTC()
33+
}
34+
return out
35+
}
36+
1837
func main() {
1938
time.Sleep(2 * time.Second)
2039
appConfig, err := godotenv.Read()
2140
if err != nil {
2241
log.Printf("Error reading .env file %s", err.Error())
2342
os.Exit(1)
24-
2543
}
2644

2745
uss.MetaStore = &uss.Store{}
@@ -36,26 +54,20 @@ func main() {
3654
func StartHTTPServer() {
3755
e := echo.New()
3856
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
39-
Format: `${remote_ip} [${time_rfc3339}] "${method} ${uri} HTTP/1.0" ${status} ${latency_human} ${bytes_out} ${error} "${user_agent}"` + "\n",
40-
Skipper: func(c echo.Context) bool {
41-
return c.Request().RequestURI == "/healthcheck"
42-
},
57+
Format: `${remote_ip} [${time_rfc3339}] "${method} ${uri} HTTP/1.0" ${status} ${latency_human} ${bytes_out} ${error} "${user_agent}"` + "\n",
58+
Skipper: func(c echo.Context) bool { return c.Request().RequestURI == "/healthcheck" },
4359
}))
4460
e.Use(middleware.Recover())
45-
e.GET("/", func(c echo.Context) error {
46-
return c.String(http.StatusOK, "Hello, World!")
47-
})
48-
e.GET("/healthcheck", func(c echo.Context) error {
49-
return c.String(http.StatusOK, "good!")
50-
})
61+
62+
e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") })
63+
e.GET("/healthcheck", func(c echo.Context) error { return c.String(http.StatusOK, "good!") })
5164

5265
e.GET("/resolve/:code", func(c echo.Context) error {
5366
code := c.Param("code")
5467
info := uss.MetaStore.FindByShortCode(code)
5568
if info != nil {
56-
return c.JSON(http.StatusOK, info)
69+
return c.JSON(http.StatusOK, utcInfo(info))
5770
}
58-
5971
return c.String(http.StatusNotFound, "Not Found.")
6072
})
6173

@@ -64,19 +76,128 @@ func StartHTTPServer() {
6476
if err := c.Bind(req); err != nil {
6577
return err
6678
}
67-
6879
req.ShortCode = uss.GenerateShortLink(req.URL)
69-
err := uss.MetaStore.Persist(req)
70-
if err != nil {
80+
if err := uss.MetaStore.Persist(req); err != nil {
7181
return c.String(http.StatusInternalServerError, fmt.Sprintf("Failed Persisiting Entity with Error %s", err.Error()))
7282
}
73-
7483
req.UpdatedAt = req.UpdatedAt.Truncate(time.Second)
75-
return c.JSON(http.StatusOK, req)
84+
return c.JSON(http.StatusOK, utcInfo(req))
85+
})
86+
87+
// Original "seed" kept (now sets CreatedBy too)
88+
e.POST("/seed", func(c echo.Context) error {
89+
end := time.Date(9999, 1, 1, 0, 0, 0, 0, time.UTC)
90+
info := &uss.ShortCodeInfo{
91+
ShortCode: "dt-sentinel-9999-01-01T00:00:00Z",
92+
URL: "https://example.com/sentinel-start",
93+
EndTime: end,
94+
CreatedBy: "keploy.io/dates",
95+
}
96+
if err := uss.MetaStore.UpsertByShortCode(info); err != nil {
97+
return c.String(http.StatusInternalServerError, err.Error())
98+
}
99+
return c.JSON(http.StatusOK, utcInfo(info))
100+
})
101+
102+
// seed a set of edge-case datetimes
103+
e.POST("/seed/dates", func(c echo.Context) error {
104+
nowMicro := uss.ToMicroUTC(time.Now())
105+
106+
// Labels become ShortCode so they’re easy to fetch directly.
107+
payload := []*uss.ShortCodeInfo{
108+
{
109+
ShortCode: "dt-sentinel-9999-01-01T00:00:00Z",
110+
URL: "https://example.com/sentinel-start",
111+
EndTime: uss.SentinelStart,
112+
CreatedBy: "keploy.io/dates",
113+
},
114+
{
115+
ShortCode: "dt-max-9999-12-31T23:59:59.999999Z",
116+
URL: "https://example.com/sentinel-max",
117+
EndTime: uss.SentinelMax,
118+
CreatedBy: "keploy.io/dates",
119+
},
120+
{
121+
ShortCode: "dt-min-1000-01-01T00:00:00Z",
122+
URL: "https://example.com/min-valid",
123+
EndTime: time.Date(1000, 1, 1, 0, 0, 0, 0, time.UTC),
124+
CreatedBy: "keploy.io/dates",
125+
},
126+
{
127+
ShortCode: "dt-epoch-1970-01-01T00:00:00Z",
128+
URL: "https://example.com/epoch",
129+
EndTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
130+
CreatedBy: "keploy.io/dates",
131+
},
132+
{
133+
ShortCode: "dt-leap-2020-02-29T12:34:56Z",
134+
URL: "https://example.com/leap",
135+
EndTime: time.Date(2020, 2, 29, 12, 34, 56, 0, time.UTC),
136+
CreatedBy: "keploy.io/dates",
137+
},
138+
{
139+
ShortCode: "dt-offset-2023-07-01T18:30:00+05:30",
140+
URL: "https://example.com/offset",
141+
EndTime: time.Date(2023, 7, 1, 18, 30, 0, 0, time.FixedZone("IST", 5*3600+30*60)),
142+
CreatedBy: "keploy.io/dates",
143+
},
144+
{
145+
ShortCode: "dt-now-trunc",
146+
URL: "https://example.com/now",
147+
EndTime: nowMicro,
148+
CreatedBy: "keploy.io/dates",
149+
},
150+
}
151+
152+
if err := uss.MetaStore.UpsertMany(payload); err != nil {
153+
return c.String(http.StatusInternalServerError, err.Error())
154+
}
155+
// Return what we wrote (UTC’d for JSON determinism)
156+
resp := make([]uss.ShortCodeInfo, 0, len(payload))
157+
for _, p := range payload {
158+
if got := uss.MetaStore.FindByShortCode(p.ShortCode); got != nil {
159+
resp = append(resp, *utcInfo(got))
160+
}
161+
}
162+
return c.JSON(http.StatusOK, resp)
163+
})
164+
165+
// exact EndTime query (RFC3339/RFC3339Nano/MySQL-like)
166+
e.GET("/query/by-endtime", func(c echo.Context) error {
167+
ts := c.QueryParam("ts")
168+
if ts == "" {
169+
return c.String(http.StatusBadRequest, "query param 'ts' required (RFC3339 or MySQL datetime)")
170+
}
171+
t, err := uss.ParseFlexible(ts)
172+
if err != nil {
173+
return c.String(http.StatusBadRequest, fmt.Sprintf("parse error: %v", err))
174+
}
175+
infos := uss.MetaStore.FindByEndTime(t)
176+
return c.JSON(http.StatusOK, utcInfos(infos))
177+
})
178+
179+
// fetch both sentinels
180+
e.GET("/query/sentinels", func(c echo.Context) error {
181+
infos := uss.MetaStore.FindSentinels()
182+
return c.JSON(http.StatusOK, utcInfos(infos))
183+
})
184+
185+
// fetch any seeded date rows
186+
e.GET("/query/dates", func(c echo.Context) error {
187+
infos := uss.MetaStore.FindSeededDates()
188+
return c.JSON(http.StatusOK, utcInfos(infos))
189+
})
190+
191+
// fetch by label (stored in ShortCode)
192+
e.GET("/query/label/:label", func(c echo.Context) error {
193+
label := c.Param("label")
194+
info := uss.MetaStore.FindByShortCode(label)
195+
if info == nil {
196+
return c.String(http.StatusNotFound, "Not Found.")
197+
}
198+
return c.JSON(http.StatusOK, utcInfo(info))
76199
})
77200

78-
// automatically add routers for net/http/pprof e.g. /debug/pprof, /debug/pprof/heap, etc.
79-
// go get github.com/hiko1129/echo-pprof
80-
//echopprof.Wrap(e)
201+
// e.g., echopprof.Wrap(e)
81202
e.Logger.Fatal(e.Start(":9090"))
82203
}

0 commit comments

Comments
 (0)