@@ -16,7 +16,7 @@ type mockStatsRepo struct {
1616 snapshots []model.ProjectStatsSnapshot
1717}
1818
19- func (m * mockStatsRepo ) Timeline (_ context.Context , projectID uuid.UUID , since time.Time ) ([]model.ProjectStatsSnapshot , error ) {
19+ func (m * mockStatsRepo ) Timeline (_ context.Context , projectID uuid.UUID , since time.Time , daily bool ) ([]model.ProjectStatsSnapshot , error ) {
2020 var result []model.ProjectStatsSnapshot
2121 for _ , s := range m .snapshots {
2222 if s .ProjectID == projectID && ! s .CapturedAt .Before (since ) {
@@ -83,12 +83,81 @@ func TestStatsTimeline_InvalidRange(t *testing.T) {
8383 info := userAuthInfo ()
8484 setupStatsProject (t , projectRepo , memberRepo , info , model .ProjectRoleMember )
8585
86- _ , err := svc .Timeline (context .Background (), info , "STAT" , "30d " )
86+ _ , err := svc .Timeline (context .Background (), info , "STAT" , "abc " )
8787 if err == nil {
8888 t .Fatal ("expected validation error for invalid range" )
8989 }
9090}
9191
92+ func TestStatsTimeline_CustomRange (t * testing.T ) {
93+ svc , _ , projectRepo , memberRepo := newTestStatsService ()
94+ info := userAuthInfo ()
95+ setupStatsProject (t , projectRepo , memberRepo , info , model .ProjectRoleMember )
96+
97+ // 30d should now be valid
98+ _ , err := svc .Timeline (context .Background (), info , "STAT" , "30d" )
99+ if err != nil {
100+ t .Fatalf ("expected no error for 30d, got %v" , err )
101+ }
102+ }
103+
104+ func TestParseSince (t * testing.T ) {
105+ tests := []struct {
106+ name string
107+ input string
108+ wantErr bool
109+ }{
110+ {"1 hour" , "1h" , false },
111+ {"24 hours" , "24h" , false },
112+ {"3 days" , "3d" , false },
113+ {"7 days" , "7d" , false },
114+ {"14 days" , "14d" , false },
115+ {"30 days" , "30d" , false },
116+ {"365 days" , "365d" , false },
117+ {"compound" , "2h30m" , false },
118+ {"30 minutes" , "30m" , false },
119+ {"empty" , "" , true },
120+ {"garbage" , "abc" , true },
121+ {"zero days" , "0d" , true },
122+ {"negative hours" , "-5h" , true },
123+ {"over max days" , "400d" , true },
124+ {"over max hours" , "9000h" , true },
125+ }
126+
127+ for _ , tt := range tests {
128+ t .Run (tt .name , func (t * testing.T ) {
129+ _ , err := parseSince (tt .input )
130+ if (err != nil ) != tt .wantErr {
131+ t .Errorf ("parseSince(%q) error = %v, wantErr %v" , tt .input , err , tt .wantErr )
132+ }
133+ })
134+ }
135+ }
136+
137+ func TestNeedsDailyGranularity (t * testing.T ) {
138+ tests := []struct {
139+ input string
140+ want bool
141+ }{
142+ {"7d" , false },
143+ {"8d" , true },
144+ {"168h" , false },
145+ {"169h" , true },
146+ {"24h" , false },
147+ {"30d" , true },
148+ {"3d" , false },
149+ }
150+
151+ for _ , tt := range tests {
152+ t .Run (tt .input , func (t * testing.T ) {
153+ got := needsDailyGranularity (tt .input )
154+ if got != tt .want {
155+ t .Errorf ("needsDailyGranularity(%q) = %v, want %v" , tt .input , got , tt .want )
156+ }
157+ })
158+ }
159+ }
160+
92161func TestStatsTimeline_NonMember (t * testing.T ) {
93162 svc , _ , projectRepo , _ := newTestStatsService ()
94163 info := userAuthInfo ()
0 commit comments