Implemented phase 6 for the POC prototype in the Website service.

This commit is contained in:
javier committed 2026-09-21 13:53:13 +02:00
1 parent 256d22df40
commit f2f445827b
40 files changed
+1906 -37

No files matched your search

@@ -140,7 +140,7 @@ struct AccountAndCollectingControllerTests {
_ = try await post(client, "/wants/\(id)/remove", "", as: mara)
try await client.execute(uri: "/wants", method: .get, headers: [.cookie: mara]) { response in
#expect(String(buffer: response.body).contains("Nothing on your list yet"))
#expect(String(buffer: response.body).contains("No wants yet"))
}
}
}
@@ -266,7 +266,7 @@ private extension AccountAndCollectingControllerTests {
let sessions = SessionStore(trade: .init(fluent: fluent), collecting: collecting)
let router = Router(context: WebsiteRequestContext.self)
router.addRoutes(CatalogueController<WebsiteRequestContext>(repository: catalogue, sessions: sessions).routes)
router.addRoutes(CatalogueController<WebsiteRequestContext>(repository: catalogue, sessions: sessions, collecting: collecting).routes)
router.addRoutes(SessionController<WebsiteRequestContext>(sessions: sessions, accounts: accounts, catalogue: catalogue).routes)
router.addRoutes(AccountController<WebsiteRequestContext>(accounts: accounts, catalogue: catalogue, sessions: sessions).routes)
router.addRoutes(CollectingController<WebsiteRequestContext>(collecting: collecting, catalogue: catalogue, sessions: sessions).routes)
@@ -282,3 +282,100 @@ private extension AccountAndCollectingControllerTests {
}
}
// MARK: - Collection, events, and recovery
extension AccountAndCollectingControllerTests {
@Test
func `keeps a collection through its form and the switches on a series`() async throws {
try await withApp { client in
let mara = try await signIn(client)
_ = try await post(client, "/collection", "series=echo-country&issues=1-3%2C+9", as: mara)
_ = try await post(client, "/series/echo-country/own", "issue=5", as: mara)
_ = try await post(client, "/series/echo-country/own", "issue=1", as: mara)
try await client.execute(uri: "/collection", method: .get, headers: [.cookie: mara]) { response in
let body = String(buffer: response.body)
// #2, #3 and #5: #9 is outside the six-issue run, and #1 was switched back off.
#expect(body.contains(">3/6<"))
#expect(body.contains("3 missing · 1 listed now"))
}
try await client.execute(uri: "/series/echo-country", method: .get, headers: [.cookie: mara]) { response in
#expect(String(buffer: response.body).contains("3 of 6 owned"))
}
}
}
@Test
func `serves the events, and narrows a search to the copies to pick up at one`() async throws {
try await withApp { client in
try await client.execute(uri: "/events", method: .get) { response in
let body = String(buffer: response.body)
#expect(response.status == .ok)
#expect(body.contains("Dutch Comic Con"))
#expect(body.contains("Comic Con Brussels"))
}
// Silverfish alone trades at Stripdagen.
try await client.execute(uri: "/search?pickup=stripdagen-haarlem&ships-from=anywhere", method: .get) { response in
let body = String(buffer: response.body)
#expect(body.contains("4 listings"))
#expect(body.contains("from 1 shop"))
}
try await client.execute(uri: "/search?pickup=no-such-event", method: .get) { response in
#expect(String(buffer: response.body).contains("No listings match"))
}
try await client.execute(uri: "/listing/nightward-1-cgc-9-8", method: .get) { response in
#expect(String(buffer: response.body).contains("or pick up at Dutch Comic Con"))
}
}
}
@Test
func `hands a recovery link to an account's email alone, good until it runs out`() async throws {
try await withApp { client in
let link = try await client.execute(
uri: "/recover",
method: .post,
headers: Self.form,
body: ByteBuffer(string: "email=Mara.V%40example.com")
) { response in
let body = String(buffer: response.body)
#expect(response.status == .ok)
#expect(response.headers[.cacheControl] == "no-store")
return String(try #require(body.firstMatch(of: /\/recover\/[0-9a-f]{64}/)).output)
}
try await client.execute(uri: link, method: .get) { response in
#expect(response.status == .ok)
#expect(String(buffer: response.body).contains("Set up a new passkey"))
}
try await client.execute(uri: "/recover/0000", method: .get) { response in
#expect(response.status == .gone)
}
try await client.execute(uri: "/recover", method: .post, headers: Self.form, body: ByteBuffer(string: "email=nobody%40example.com")) { response in
let body = String(buffer: response.body)
#expect(body.contains("No account has that email"))
#expect(body.firstMatch(of: /\/recover\/[0-9a-f]{64}/) == nil)
}
try await client.execute(uri: "/recover", method: .post, headers: Self.form, body: ByteBuffer(string: "email=nope")) { response in
#expect(response.status == .unprocessableContent)
}
}
}
}
@@ -141,7 +141,11 @@ struct TradeControllerTests {
// The copy is off sale, for everyone.
try await client.execute(uri: listing, method: .get) { response in
#expect(String(buffer: response.body).contains("This copy is sold."))
let body = String(buffer: response.body)
#expect(body.contains("This copy sold on "))
// Three other copies of Nightward #1 remain listed, none of them in this shop.
#expect(body.contains("has 0 more copies of Nightward #1, and 3 are listed across CCN."))
}
// A step out of turn changes nothing.
@@ -0,0 +1,55 @@
import Foundation
import Persistence
import Testing
@testable import WebsiteLibrary
@Suite(
"RecoveryLinks type",
.tags(.type)
)
struct RecoveryLinksTests {
// MARK: Methods tests
@Test
func `hands out a link that can be looked at, and used once`() async {
let links = RecoveryLinks()
let token = await links.make(for: .mara)
let peeked = await links.user(for: token)
let stillGood = await links.user(for: token)
let redeemed = await links.redeem(token)
let again = await links.redeem(token)
#expect(token.count == 64)
#expect(peeked == .mara)
#expect(stillGood == .mara)
#expect(redeemed == .mara)
#expect(again == nil)
}
@Test
func `lets a link run out after an hour, and ends an account's older link`() async {
let clock = Clock()
let links = RecoveryLinks(now: clock.now)
let older = await links.make(for: .mara)
let newer = await links.make(for: .mara)
let other = await links.make(for: .joost)
let replaced = await links.user(for: older)
clock.advance(by: 3_599)
let inTime = await links.user(for: newer)
clock.advance(by: 1)
let tooLate = await links.redeem(newer)
let othersToo = await links.user(for: other)
#expect(replaced == nil)
#expect(inTime == .mara)
#expect(tooLate == nil)
#expect(othersToo == nil)
}
}