Skip to content

Optional varighet with start–end display, edge-case tests, CI bumps - #102

Merged
RohaanMooken merged 16 commits into
mainfrom
dev
Aug 11, 2026
Merged

Optional varighet with start–end display, edge-case tests, CI bumps#102
RohaanMooken merged 16 commits into
mainfrom
dev

Conversation

@RohaanMooken

Copy link
Copy Markdown
Contributor

Summary

Everything on dev since the last release, in three groups:

Event duration (varighet) is now optional

  • Varighet can be left blank when creating or editing an event. Blank means "no known end time"; a value present must still parse and stay within the one-week cap.
  • When a duration is set, the card shows a start–end span under "Når" (tirsdag 1. juli kl 17:15–19:45, spelling out both ends if it crosses midnight). Without one, it shows just the start time as before.
  • A no-duration event's derived end falls back to midnight (Oslo) after the start. An end equal to the start would mark the event "Ferdig" the moment it begins, drop it from the listing six hours after starting, be refused by Discord (external scheduled events must end after they start), and put a zero-length sliver in the iCal feed.
  • The edit form pre-fills a no-duration event as blank rather than "0".
  • No migration needed: existing documents all carry a positive duration and stored end, and documents missing the duration key decode to 0 and render as before.

Test hardening

  • Edge-case tests across every package, plus fixes for the real bugs they uncovered (6b25b80, 10e0fe4).

CI / Dependabot

  • GitHub Actions bumps (checkout, setup-go, docker build/buildx/login/metadata actions).
  • Dependabot now opens dependency PRs against dev instead of main.

Test plan

  • go test ./... — all 17 packages pass
  • Manual check on staging: create an event with and without varighet; verify the card shows a span vs. a bare start time, and that Discord sync succeeds for a no-duration event

🤖 Generated with Claude Code

dependabot Bot and others added 16 commits August 7, 2026 11:00
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.7.1 to 4.2.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](docker/setup-buildx-action@c47758b...bb05f3f)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.9.0 to 7.3.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](docker/build-push-action@4f58ea7...53b7df9)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.1.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](actions/setup-go@41dfa10...b7ad1da)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@11bd719...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Dependabot reads this file from the default branch, so the setting lives
on main even though the pull requests land on dev.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Open dependency updates against dev instead of main
…ker/setup-buildx-action-4.2.0

Bump docker/setup-buildx-action from 3.7.1 to 4.2.0
…ker/build-push-action-7.3.0

Bump docker/build-push-action from 6.9.0 to 7.3.0
…ions/setup-go-7.0.0

Bump actions/setup-go from 5.1.0 to 7.0.0
…ions/checkout-7.0.1

Bump actions/checkout from 4.2.2 to 7.0.1
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.3.0 to 4.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@9780b0c...dbcb813)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.6.1 to 6.2.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](docker/metadata-action@369eb59...dc80280)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Raises the packages that were largely untested: api 0%->100%, config
3.7%->100%, users 32%->100%, assets 67%->100%, httpx 15%->97%, discord
12%->97%, store 60%->96%, auth 50%->94%, events 48%->87%, cmd 21%->52%.

The tests are written against current behaviour only. Several document
bugs rather than assert they are correct; each of those calls t.Skip with
an explanation once the bug is fixed, so it reports rather than blocks.

Two packages needed a seam to reach their HTTP paths without a network.
discord.APIBase is a constant, so discord and users both drive the client
through its nil Transport, swapped for a RoundTripper that rewrites the
host and fails the test on any host other than the expected one -- which
makes "never touches the network" a checked property. The Mongo query
tests assert the struct-tag to query-string correspondence directly, so a
field rename breaks loudly without a live server.

Verified with -race, -count=2 and -shuffle=on. Nine deliberate defects
injected into production code (dropped CSRF empty-cookie guard, disabled
alg check, unchecked JWT signature, removed Vary, downgraded board-only
gate, dropped login gate, immutable dev assets, cleared HttpOnly, skipped
Mongo credential redaction) were all caught.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correctness and availability:

- api: ?page discarded strconv.Atoi's error, and on overflow Atoi returns
  both MaxInt64 and an error. page * PageSize then wrapped to a negative
  Skip, which the driver rejects -- an anonymous caller could turn a query
  string into a 500. Clamped to [1, maxPage].
- cmd: -healthcheck built its URL by concatenating a port, but resolveAddr
  accepts a bare port, :port or host:port. LISTEN=:3000 -- documented in
  .env.example -- produced http://127.0.0.1::3000/healthz, so the container
  reported unhealthy while serving normally. Probe and listener now come
  from one exported ResolveAddr, split with net.SplitHostPort. :: probes
  ::1 rather than 127.0.0.1, which an IPv6-only bind would not answer.
- httpx: a zero window made Allow divide by zero and store NaN in the
  bucket, after which every comparison was false and that client was never
  refused again. A limiter that looks installed but never limits is the
  failure mode the type exists to prevent, so NewRateLimiter now panics on
  a non-positive burst or window; both call sites pass constants.
- validate: NaN parses and compares false against both bounds, so it passed
  any range. As a duration it became an end time in 1733, and the event
  read as finished and vanished from the listing on save.
- config: PORT/LISTEN outside 1-65535 now fail at startup rather than in
  ListenAndServe. PORT=0 is refused: the kernel picks a port the health
  check cannot find.

Wrong thing said to the user:

- api, web: fusionauth.parseError wraps every non-2xx, non-404 reply in
  *APIError, so errors.As matched during an outage and the member was told
  their registration was invalid. Now split on apiErr.Status.
- discord: any 401 claimed the bot token was rejected, but Exchange and
  currentUser are never sent it -- a reloaded callback pointed operators at
  the wrong variable. The error now carries which credential was used.
- users: a failed guild-membership check was persisted as isMember:false,
  so the next page told a member to join a server they were already in.
  The key is now omitted and the last known value kept.

Hardening:

- users: the email check rejected space and tab but not \n, \r or NUL, and
  had no length bound; the address is forwarded to an upstream that sends
  mail. Now rejects all control and space runes, capped at 254.
- auth: Inject left a cookie it could not open in the jar, to be re-sent
  forever. It now clears it, which is what its doc comment always claimed.
- auth: quoteJSON escaped only " \ and \n, so any control character made
  the error body unparseable. Replaced with encoding/json.
- auth: ParseForm does not read multipart bodies, so every multipart POST
  would have 403'd as an expired form. The body bound is unchanged.
- discord: the 429 wait was uncapped on a path carrying a request context
  with no deadline, parking the handler for the whole window. Capped at
  5s, and the three linking handlers now carry a 30s deadline.
- httpx: gzip mis-parsed RFC-legal whitespace before a q-value and treated
  q=0 as acceptance; a 206 could also be re-encoded by the middleware.
- httpx: RateLimiter leaked a goroutine and a ticker per instance.
- config: a UTF-8 BOM made the first .env key unreadable, so FUSION_AUTH_
  HOST -- the first line of .env.example -- was reported missing.

Documented rather than changed: LISTEN is spent twice by docker-compose,
as the host side of the port mapping and, via env_file, as the port the
process binds; the container side is hardcoded to 3000, so only a bare
3000 works there. Left alone: linking one Discord account to several
members, which needs a product decision -- RemoveMemberRole works by
Discord id alone, so one member unlinking strips the shared account's
guild role from the other.

Verified with -race and MONGO_TEST_URL against MongoDB 8, matching CI.
Eleven mutations of the new guards were each caught by a named test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Events without a duration still render just the start time under "Når".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A blank duration is accepted and means the end is unknown: the card shows
just the start time, and the edit form pre-fills the field as blank
rather than "0".

Since end-equals-start would mark a no-duration event "Ferdig" the
moment it begins, drop it from the listing six hours after it starts,
send Discord a scheduled event that ends when it starts (which it
refuses), and put a zero-length sliver in the iCal feed, ComputeEnd now
falls back to midnight in Oslo after the start when no duration is set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@RohaanMooken
RohaanMooken merged commit b219dbf into main Aug 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants