chore: track go.mod, go.sum, and missing test files
These were never committed (hidden by status.showUntrackedFiles=no): without go.mod/go.sum a fresh clone would not build, and the source files committed in abb38e3 were missing their accompanying tests.
This commit is contained in:
parent
2e75d15f9d
commit
74cb302f64
|
|
@ -0,0 +1,36 @@
|
||||||
|
module git.kambr.pl/kamash/dyzur-bot.git
|
||||||
|
|
||||||
|
go 1.26.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-telegram/bot v1.21.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
google.golang.org/api v0.285.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/auth v0.20.0 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
|
golang.org/x/crypto v0.53.0 // indirect
|
||||||
|
golang.org/x/net v0.56.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
|
golang.org/x/text v0.38.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
|
||||||
|
google.golang.org/grpc v1.81.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
||||||
|
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-telegram/bot v1.21.0 h1:Va/PbGc2vBDdv57GCUEEVV6ROlHWiC6SklJY9Hvhzps=
|
||||||
|
github.com/go-telegram/bot v1.21.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
|
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/googleapis/enterprise-certificate-proxy v0.3.16 h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||||
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/api v0.285.0 h1:B7eHHoKGAX/LrPkQvhQqnGwjgWxofbdGwCTQvpm8FkM=
|
||||||
|
google.golang.org/api v0.285.0/go.mod h1:NlOlUIr8MPoIhT9Bb/oUnRuHbJOLwxb6JSYJM8Yz+jQ=
|
||||||
|
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||||
|
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
package dyzurbot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func warsaw(t *testing.T) *time.Location {
|
||||||
|
t.Helper()
|
||||||
|
loc, err := time.LoadLocation("Europe/Warsaw")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Europe/Warsaw tzdata unavailable: %v", err)
|
||||||
|
}
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextFire(t *testing.T) {
|
||||||
|
loc := warsaw(t)
|
||||||
|
at := func(y int, mo time.Month, d, h, mi int) time.Time {
|
||||||
|
return time.Date(y, mo, d, h, mi, 0, 0, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
now time.Time
|
||||||
|
weekday time.Weekday
|
||||||
|
hour int
|
||||||
|
want time.Time
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"same weekday before hour -> today",
|
||||||
|
at(2026, 6, 15, 8, 0), time.Monday, 9, // Mon 08:00
|
||||||
|
at(2026, 6, 15, 9, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"same weekday exactly at hour -> next week (strictly after)",
|
||||||
|
at(2026, 6, 15, 9, 0), time.Monday, 9,
|
||||||
|
at(2026, 6, 22, 9, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"same weekday after hour -> next week",
|
||||||
|
at(2026, 6, 15, 10, 0), time.Monday, 9,
|
||||||
|
at(2026, 6, 22, 9, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"later weekday -> upcoming target weekday",
|
||||||
|
at(2026, 6, 17, 12, 0), time.Monday, 9, // Wed -> next Mon
|
||||||
|
at(2026, 6, 22, 9, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"day before -> next day",
|
||||||
|
at(2026, 6, 21, 23, 0), time.Monday, 9, // Sun 23:00 -> Mon 09:00
|
||||||
|
at(2026, 6, 22, 9, 0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := nextFire(tc.now, tc.weekday, tc.hour, loc)
|
||||||
|
if !got.Equal(tc.want) {
|
||||||
|
t.Errorf("nextFire = %s, want %s", got.In(loc), tc.want.In(loc))
|
||||||
|
}
|
||||||
|
if !got.After(tc.now) {
|
||||||
|
t.Errorf("nextFire %s not strictly after now %s", got.In(loc), tc.now.In(loc))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Across the spring-forward (2026-03-29 in Warsaw, clocks jump 02:00->03:00),
|
||||||
|
// the next Monday 09:00 must still be 09:00 wall-clock in Warsaw, proving the
|
||||||
|
// fire time is recomputed in the zone rather than by +7d UTC arithmetic.
|
||||||
|
func TestNextFireAcrossDST(t *testing.T) {
|
||||||
|
loc := warsaw(t)
|
||||||
|
now := time.Date(2026, 3, 28, 12, 0, 0, 0, loc) // Sat before DST
|
||||||
|
got := nextFire(now, time.Monday, 9, loc).In(loc)
|
||||||
|
|
||||||
|
if got.Weekday() != time.Monday || got.Hour() != 9 || got.Minute() != 0 {
|
||||||
|
t.Fatalf("got %s, want Monday 09:00 Warsaw", got)
|
||||||
|
}
|
||||||
|
if y, m, d := got.Date(); y != 2026 || m != time.March || d != 30 {
|
||||||
|
t.Errorf("got date %04d-%02d-%02d, want 2026-03-30", y, m, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReminderText(t *testing.T) {
|
||||||
|
// Week of 2026-06-29; target lands inside it.
|
||||||
|
target := day(t, "2026-06-30")
|
||||||
|
rota := func(p1, p2 string) Rota {
|
||||||
|
return Rota{Weeks: []DutyWeek{week(t, "2026-06-29", p1, p2)}}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rota Rota
|
||||||
|
target time.Time
|
||||||
|
wantOK bool
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"both people",
|
||||||
|
rota("Ala", "Bartek"), target, true,
|
||||||
|
"Przypomnienie: w tygodniu od 2026-06-29 dyżur sprzątają Ala i Bartek.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"one person",
|
||||||
|
rota("Ala", ""), target, true,
|
||||||
|
"Przypomnienie: w tygodniu od 2026-06-29 dyżur sprząta Ala.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nobody assigned",
|
||||||
|
rota("", ""), target, true,
|
||||||
|
"Przypomnienie: w tygodniu od 2026-06-29 dyżur jest nieobsadzony.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no matching week -> skip",
|
||||||
|
rota("Ala", "Bartek"), day(t, "2026-09-01"), false,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, ok := reminderText(tc.rota, tc.target)
|
||||||
|
if ok != tc.wantOK {
|
||||||
|
t.Fatalf("ok = %v, want %v", ok, tc.wantOK)
|
||||||
|
}
|
||||||
|
if ok && got != tc.want {
|
||||||
|
t.Errorf("reminderText = %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWeekday(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
in string
|
||||||
|
want time.Weekday
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"Monday", time.Monday, false},
|
||||||
|
{"Sunday", time.Sunday, false},
|
||||||
|
{"monday", time.Monday, false}, // case-insensitive
|
||||||
|
{"Funday", 0, true},
|
||||||
|
{"", 0, true},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.in, func(t *testing.T) {
|
||||||
|
got, err := parseWeekday(tc.in)
|
||||||
|
if (err != nil) != tc.wantErr {
|
||||||
|
t.Fatalf("err = %v, wantErr %v", err, tc.wantErr)
|
||||||
|
}
|
||||||
|
if err == nil && got != tc.want {
|
||||||
|
t.Errorf("parseWeekday(%q) = %v, want %v", tc.in, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package dyzurbot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rows mirrors the real "Dyżury" sheet: cols A–C are the duty schedule,
|
||||||
|
// cols E–F are the person roster, with UNFORMATTED_VALUE rendering the
|
||||||
|
// week date and duty count as float64 and trailing empty cells dropped.
|
||||||
|
func sampleRows() [][]interface{} {
|
||||||
|
return [][]interface{}{
|
||||||
|
// row 0: header — must be skipped
|
||||||
|
{"Tydzień zaczynający się od", "Osoba 1", "Osoba 2", "", "Lista osób", "Liczba dyżurów"},
|
||||||
|
// full week + roster entry
|
||||||
|
{float64(46027), "Jan Kowalski", "Anna Nowak", "", "Piotr Wiśniewski", float64(1)},
|
||||||
|
// empty week (no one assigned) + roster entry
|
||||||
|
{float64(46034), "", "", "", "Maria Wójcik", float64(0)},
|
||||||
|
// one-person week, ragged row (no roster columns at all)
|
||||||
|
{float64(46041), "Tomasz Zieliński"},
|
||||||
|
// week present, blank roster slot with a count -> person skipped
|
||||||
|
{float64(46048), "", "", "", "", float64(0)},
|
||||||
|
// roster-only row (week column empty) -> person added, no week
|
||||||
|
{"", "", "", "", "Katarzyna Lewandowska", float64(3)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRotaSkipsHeaderAndCountsWeeks(t *testing.T) {
|
||||||
|
rota := ParseRota(sampleRows())
|
||||||
|
|
||||||
|
// rows 1..4 carry a week serial; row 5's week column is empty
|
||||||
|
if got := len(rota.Weeks); got != 4 {
|
||||||
|
t.Fatalf("weeks: got %d, want 4", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRotaDecodesWeekStartToMonday(t *testing.T) {
|
||||||
|
rota := ParseRota(sampleRows())
|
||||||
|
|
||||||
|
want := time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC) // serial 46027 = Mon 2026-01-05
|
||||||
|
if got := rota.Weeks[0].WeekStart; !got.Equal(want) {
|
||||||
|
t.Errorf("week0 start: got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRotaWeekPersons(t *testing.T) {
|
||||||
|
rota := ParseRota(sampleRows())
|
||||||
|
|
||||||
|
if w := rota.Weeks[0]; w.Person1 != "Jan Kowalski" || w.Person2 != "Anna Nowak" {
|
||||||
|
t.Errorf("full week persons: got %q/%q", w.Person1, w.Person2)
|
||||||
|
}
|
||||||
|
if w := rota.Weeks[1]; w.Person1 != "" || w.Person2 != "" {
|
||||||
|
t.Errorf("empty week should keep blank persons: got %q/%q", w.Person1, w.Person2)
|
||||||
|
}
|
||||||
|
if w := rota.Weeks[2]; w.Person1 != "Tomasz Zieliński" || w.Person2 != "" {
|
||||||
|
t.Errorf("one-person ragged week: got %q/%q", w.Person1, w.Person2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRotaRosterSkipsBlankSlots(t *testing.T) {
|
||||||
|
rota := ParseRota(sampleRows())
|
||||||
|
|
||||||
|
// Piotr Wiśniewski, Maria Wójcik, Katarzyna Lewandowska — the blank-name slot is skipped
|
||||||
|
if got := len(rota.People); got != 3 {
|
||||||
|
t.Fatalf("people: got %d, want 3", got)
|
||||||
|
}
|
||||||
|
if p := rota.People[0]; p.Name != "Piotr Wiśniewski" || p.DutyCount != 1 {
|
||||||
|
t.Errorf("person0: got %+v, want {Piotr Wiśniewski 1}", p)
|
||||||
|
}
|
||||||
|
if p := rota.People[2]; p.Name != "Katarzyna Lewandowska" || p.DutyCount != 3 {
|
||||||
|
t.Errorf("person2: got %+v, want {Katarzyna Lewandowska 3}", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
package dyzurbot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// week is a small helper to build a DutyWeek from a YYYY-MM-DD start.
|
||||||
|
func week(t *testing.T, start, p1, p2 string) DutyWeek {
|
||||||
|
t.Helper()
|
||||||
|
ws, err := time.Parse("2006-01-02", start)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad date %q: %v", start, err)
|
||||||
|
}
|
||||||
|
return DutyWeek{WeekStart: ws, Person1: p1, Person2: p2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func day(t *testing.T, d string) time.Time {
|
||||||
|
t.Helper()
|
||||||
|
tm, err := time.Parse("2006-01-02", d)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad date %q: %v", d, err)
|
||||||
|
}
|
||||||
|
return tm
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCurrentDuty(t *testing.T) {
|
||||||
|
rota := Rota{Weeks: []DutyWeek{
|
||||||
|
week(t, "2026-06-15", "Ala", "Bartek"),
|
||||||
|
week(t, "2026-06-22", "Celina", "Darek"),
|
||||||
|
}}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
today time.Time
|
||||||
|
wantOK bool
|
||||||
|
wantStart string // WeekStart of the matched week, when wantOK
|
||||||
|
}{
|
||||||
|
{"inside first week", day(t, "2026-06-17"), true, "2026-06-15"},
|
||||||
|
{"first day of first week", day(t, "2026-06-15"), true, "2026-06-15"},
|
||||||
|
{"last day of first week", day(t, "2026-06-21"), true, "2026-06-15"},
|
||||||
|
{"first day of second week", day(t, "2026-06-22"), true, "2026-06-22"},
|
||||||
|
{"before all weeks", day(t, "2026-06-14"), false, ""},
|
||||||
|
{"after all weeks", day(t, "2026-06-29"), false, ""},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, ok := currentDuty(rota, tc.today)
|
||||||
|
if ok != tc.wantOK {
|
||||||
|
t.Fatalf("ok = %v, want %v", ok, tc.wantOK)
|
||||||
|
}
|
||||||
|
if ok && got.WeekStart.Format("2006-01-02") != tc.wantStart {
|
||||||
|
t.Errorf("matched week %s, want %s",
|
||||||
|
got.WeekStart.Format("2006-01-02"), tc.wantStart)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCurrentDutyEmptyRota(t *testing.T) {
|
||||||
|
if _, ok := currentDuty(Rota{}, day(t, "2026-06-17")); ok {
|
||||||
|
t.Error("empty rota should not match any week")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A local time just after midnight in a +02:00 zone is still the previous day
|
||||||
|
// in UTC. currentDuty must match on the local calendar date, so 2026-06-15
|
||||||
|
// (Poland) lands in the week starting 2026-06-15, not the prior week.
|
||||||
|
func TestCurrentDutyLocalMidnight(t *testing.T) {
|
||||||
|
rota := Rota{Weeks: []DutyWeek{week(t, "2026-06-15", "Ala", "Bartek")}}
|
||||||
|
today := time.Date(2026, 6, 15, 0, 30, 0, 0, time.FixedZone("CEST", 2*3600))
|
||||||
|
|
||||||
|
got, ok := currentDuty(rota, today)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected a match for local 2026-06-15")
|
||||||
|
}
|
||||||
|
if got.Person1 != "Ala" {
|
||||||
|
t.Errorf("matched wrong week: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatDuty(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
week DutyWeek
|
||||||
|
ok bool
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"both people",
|
||||||
|
week(t, "2026-06-15", "Ala", "Bartek"), true,
|
||||||
|
"Dyżur w tym tygodniu (od 2026-06-15): Ala i Bartek",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"only person1",
|
||||||
|
week(t, "2026-06-15", "Ala", ""), true,
|
||||||
|
"Dyżur w tym tygodniu (od 2026-06-15): Ala",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"only person2",
|
||||||
|
week(t, "2026-06-15", "", "Bartek"), true,
|
||||||
|
"Dyżur w tym tygodniu (od 2026-06-15): Bartek",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nobody assigned",
|
||||||
|
week(t, "2026-06-15", "", ""), true,
|
||||||
|
"Dyżur w tym tygodniu (od 2026-06-15): brak przypisania",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"not found",
|
||||||
|
DutyWeek{}, false,
|
||||||
|
"Nie znalazłem dyżuru na ten tydzień.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := formatDuty(tc.week, tc.ok); got != tc.want {
|
||||||
|
t.Errorf("formatDuty = %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue