Skip to content

Commit

Permalink
more tests and theoretically fix the notification bug
Browse files Browse the repository at this point in the history
  • Loading branch information
cohoe committed Jan 18, 2025
1 parent a33b9f5 commit 1434d44
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 70 deletions.
39 changes: 27 additions & 12 deletions Sources/swiftarr/Helpers/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,27 +251,38 @@ extension Settings {
return cal
}

// Determines the UTC offset in seconds from the cruiseStartDate to the given date.
// When the given date and the cruiseStartDate are in the same time zone and Daylight
// Savings mode the offset should be 0. Moving into another time zone (such as Atlantic
// Time) or engaging Daylight Savings Time will adjust this offset. Positive number
// is towards UTC (ex: UTC-5 to UTC-4) while a negative number is away from UTC
// (ex: UTC-4 to UTC-5)
func getPortOffset(_ date: Date) -> Int {
let portUtcOffset = timeZoneChanges.tzAtTime(cruiseStartDate()).secondsFromGMT(for: cruiseStartDate())
let dateUtcOffset = timeZoneChanges.tzAtTime(date).secondsFromGMT(for: date)
return dateUtcOffset - portUtcOffset
}

// Generate a `Date` that lets us pretend we are at that point in time during the sailing.
// It can be difficult to test schedule functionality because the events are all coded for
// their actual times. So at various points in the app we display the data of "what would be".
// This takes it a step further and pretends based on the time rather than just a weekday.
func getDateInCruiseWeek(from date: Date = Date()) -> Date {
// @TODO Ensure this honors or passes sanity check for portTimeZone or something like that.
// It's probably OK, but we should be sure.
let secondsPerWeek = 60 * 60 * 24 * 7
let partialWeek = Int(date.timeIntervalSince(Settings.shared.cruiseStartDate())) % secondsPerWeek
// When startDate is in the future, the partialWeek is negative. Which if taken at face value returns
// the current date (start - time since start = now). When startDate is in the past, the partialWeek is
// positive. Since the whole point of this functionality is to time travel, we abs() it.
return Settings.shared.cruiseStartDate() + abs(TimeInterval(partialWeek))
var partialWeek = Int(date.timeIntervalSince(Settings.shared.cruiseStartDate())) % secondsPerWeek
// When cruiseStartDate is in the future, the partialWeek is negative.
// When cruiseStartDate is in the past, the partialWeek is positive.
if (partialWeek < 0) {
partialWeek += secondsPerWeek
}
return Settings.shared.cruiseStartDate() + TimeInterval(partialWeek)
}

// This is sufficiently complex enough to merit its own function. Unlike the Settings.shared.getDateInCruiseWeek(),
// just adding .seconds to Date() isn't enough because Date() returns millisecond-precision. Which no one tells you
// unless you do a .timeIntervalSince1970 and get the actual Double value back to look at what's behind the dot.
// I'm totally not salty about spending several hours chasing this down. Anywho...
// This takes the current Date(), strips the ultra-high-precision that we don't want, and returns Date() with the
// upcoming notification offset applied.
// This takes the current Date(), strips the ultra-high-precision that we don't want, and returns Date().
func getCurrentFilterDate(from date: Date = Date()) -> Date {
let todayCalendar = Settings.shared.calendarForDate(date)
let todayComponents = todayCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
Expand All @@ -282,7 +293,7 @@ extension Settings {
// Often in the code we use Date() to get the current time, but this can sometimes cause
// strange behavior because we occasionally pretend to be in the cruise week.
func getScheduleReferenceDate(_ settingType: EventNotificationSetting) -> Date {
// The filter date is calculated by adding the notification offset interval to either:
// The reference date is calculated as either:
// 1) The current date (as in what the server is experiencing right now).
// 2) The current time/day transposed within the cruise week (when we pretend it is).
var filterDate: Date
Expand All @@ -309,8 +320,7 @@ extension Settings {
}
try await storedSetting.readFromRedis(redis: app.redis)
}
// TimeZoneChanges load in from postgres
timeZoneChanges = try await TimeZoneChangeSet(app.db)
try await readTimeZoneChanges(app)
}

// Stores all settings to Redis
Expand All @@ -321,6 +331,11 @@ extension Settings {
}
}
}

// TimeZoneChanges load in from postgres
func readTimeZoneChanges(_ app: Application) async throws {
timeZoneChanges = try await TimeZoneChangeSet(app.db)
}
}

// To maintain thread safety, this 1-value struct needs to be immutable, so that mutating Settings.disabledFeatures
Expand Down
10 changes: 5 additions & 5 deletions Sources/swiftarr/seeds/time-zone-changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
# information than timezone abbreviations, such as whether an area observers DST.
#
# 2024 sailing, assumed
20240120T070000Z EST America/New_York
20240121T070000Z EDT America/New_York
20240123T060000Z EDT America/Grand_Turk
20240124T060000Z AST America/Santo_Domingo
20240125T070000Z EDT America/New_York
20240309T070000Z EST America/New_York
20240310T070000Z EDT America/New_York
20240312T060000Z EDT America/Grand_Turk
20240313T060000Z AST America/Santo_Domingo
20240314T070000Z EDT America/New_York
3 changes: 3 additions & 0 deletions Tests/AppTests/Application+Testable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ extension Application {
let app = try await Application.make(.testing)
do {
try await SwiftarrConfigurator(app).configure()
// Some day this should do the entire post startup configuration.
// Not just the TZChanges that I cherry-picked out to make them function.
try await Settings.shared.readTimeZoneChanges(app)
} catch {
app.logger.report(error: error)
try? await app.asyncShutdown()
Expand Down
145 changes: 92 additions & 53 deletions Tests/AppTests/SettingsTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@testable import swiftarr
import XCTVapor
import Testing
import XCTVapor

@testable import swiftarr

// In 2024:
// Embarkation Day 2024-03-09 (Saturday)
Expand All @@ -16,67 +17,105 @@ class SettingsTests: XCTestCase, SwiftarrBaseTest {
// Midnight UTC on the day that we depart port.
let embarkationDate = ISO8601DateFormatter().date(from: "2024-03-09T05:00:00Z")!

func testEmbarkationStartDate() {
XCTAssertEqual(embarkationDate, Settings.shared.cruiseStartDate())
}
func testEmbarkationStartDate() async throws {
try await withApp { app in
XCTAssertEqual(embarkationDate, Settings.shared.cruiseStartDate())
}
}

func testPresentEmbarkationDate() {
let testDate = embarkationDate
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(embarkationDate, resultDate)
}
func testTimeZoneChange() async throws {
try await withApp { app in
let testDate = ISO8601DateFormatter().date(from: "2024-03-12T06:00:00Z")!
XCTAssertEqual(Settings.shared.timeZoneChanges.tzAtTime(testDate).identifier, "America/Grand_Turk")
}
}

func testPastEmbarkationDate() {
let testDate = ISO8601DateFormatter().date(from: "2023-07-08T05:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(embarkationDate, resultDate)
}
func testPresentEmbarkationDate() async throws {
try await withApp { app in
let testDate = embarkationDate
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(embarkationDate, resultDate)
}
}

func testFutureEmbarkationDate() {
let testDate = ISO8601DateFormatter().date(from: "2025-01-11T05:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(embarkationDate, resultDate)
}
func testPastEmbarkationDate() async throws {
try await withApp { app in
let testDate = ISO8601DateFormatter().date(from: "2023-07-08T05:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(embarkationDate, resultDate)
}
}

// The day we go to DST, 11:00AM UTC 7:00AM UTC-4 (EDT, EST+DST, AST)
// "Theme: Retro Day" uses this date so its an easy checkpoint. In the database
// it is stored as 2024-03-10T11:00:00Z. Whereas "Theme: Welcome, New Cruisers!"
// starts at "2024-03-09 12:00:00+00" in the DB at 12:00PM UTC aka 7:00AM UTC-5, EST.
let dstDate = ISO8601DateFormatter().date(from: "2024-03-10T11:00:00Z")!
func testFutureEmbarkationDate() async throws {
try await withApp { app in
let testDate = ISO8601DateFormatter().date(from: "2025-01-11T05:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(embarkationDate, resultDate)
}
}

func testPresentDstDate() {
let testDate = dstDate
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(dstDate, resultDate)
}
// The day we go to DST, 11:00AM UTC 7:00AM UTC-4 (EDT, EST+DST, AST)
// "Theme: Retro Day" uses this date so its an easy checkpoint. In the database
// it is stored as 2024-03-10T11:00:00Z. Whereas "Theme: Welcome, New Cruisers!"
// starts at "2024-03-09 12:00:00+00" in the DB at 12:00PM UTC aka 7:00AM UTC-5, EST.
let dstDate = ISO8601DateFormatter().date(from: "2024-03-10T11:00:00Z")!

// @TODO validate that the date times are correct in reality.
func testPresentDstDate() async throws {
try await withApp { app in
let testDate = dstDate
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(dstDate, resultDate)
}
}

// This date is in the past but is still within daylight time (UTC-4)
func testPastDstDate() {
// This date is in the past but is still within daylight time (UTC-4)
// 2023 July 09 is a Sunday
func testPastDstDate() async throws {
let testDate = ISO8601DateFormatter().date(from: "2023-07-09T11:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(dstDate, resultDate)
}
}

// This date is in the past but back in standard time (UTC-5)
func testPastNonDstDate() {
let testDate = ISO8601DateFormatter().date(from: "2023-11-12T11:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(dstDate, resultDate)
}
// This date is in the past but back in standard time (UTC-5)
func testPastNonDstDate() async throws {
try await withApp { app in
let testDate = ISO8601DateFormatter().date(from: "2023-11-12T11:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(dstDate, resultDate)
}
}

// This date is in the future but is still within daylight time (UTC-4)
func testFutureDstDate() {
let testDate = ISO8601DateFormatter().date(from: "2024-05-12T11:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(dstDate, resultDate)
}
// This date is in the future but is still within daylight time (UTC-4)
func testFutureDstDate() async throws {
try await withApp { app in
let testDate = ISO8601DateFormatter().date(from: "2024-05-12T11:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(dstDate, resultDate)
}
}

// This date is the future but back in standard time (UTC-5)
func testFutureNonDstDate() {
let testDate = ISO8601DateFormatter().date(from: "2025-01-12T11:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(dstDate, resultDate)
}
}
// This date is the future but back in standard time (UTC-5)
func testFutureNonDstDate() async throws {
try await withApp { app in
let testDate = ISO8601DateFormatter().date(from: "2025-01-12T11:00:00Z")!
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
XCTAssertEqual(dstDate, resultDate)
}
}

// That "now" (the time that this test function was called) translates successfully
// into the cruise week.
func testNowDate() async throws {
try await withApp { app in
let testDate = Date()
let cal = Settings.shared.calendarForDate(testDate)
let testDateComponents = cal.dateComponents([.minute, .hour], from: testDate)
let resultDate = Settings.shared.getDateInCruiseWeek(from: testDate)
let resultDateComponents = cal.dateComponents([.minute, .hour], from: resultDate)

// We only really care about the hour. Minute is there for fun.
XCTAssertEqual(testDateComponents.hour, resultDateComponents.hour)
XCTAssertEqual(testDateComponents.minute, resultDateComponents.minute)
}
}
}
22 changes: 22 additions & 0 deletions scripts/docker-compose-instance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ services:
volumes:
- redis_data:/bitnami/redis/data

postgres-test:
image: docker.io/bitnami/postgresql:14
environment:
POSTGRESQL_DATABASE: swiftarr-test
POSTGRESQL_USERNAME: swiftarr
POSTGRESQL_PASSWORD: password
ports:
- 5433:5432
volumes:
- postgres_test_data:/bitnami/postgresql

redis-test:
image: docker.io/bitnami/redis:6.2
ports:
- 6380:6379
environment:
REDIS_PASSWORD: password
volumes:
- redis_test_data:/bitnami/redis/data

prometheus:
image: docker.io/bitnami/prometheus:2.48.0
ports:
Expand All @@ -43,3 +63,5 @@ services:
volumes:
postgres_data:
redis_data:
postgres_test_data:
redis_test_data:

0 comments on commit 1434d44

Please sign in to comment.