Implemented phase 1 for the POC prototype in the Website service.
This commit is contained in:
1 parent
6d7ab4bcba
commit
ea89ea892a
37 files changed
+4173
-43
No files matched your search
+99
@@ -0,0 +1,99 @@
|
||||
import Elementary
|
||||
import Persistence
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"Catalogue components",
|
||||
.tags(.component)
|
||||
)
|
||||
struct CatalogueComponentsTests {
|
||||
|
||||
// MARK: Cover tests
|
||||
|
||||
@Test
|
||||
func `letters a cover line by line on its tone`() {
|
||||
let html = Cover(Listing.stub().cover, modifier: "cover--hero").render()
|
||||
|
||||
#expect(html.contains(#"class="cover cover--navy cover--hero""#))
|
||||
#expect(html.contains("<span>NIGHT</span><span>WARD</span>"))
|
||||
#expect(html.contains(#"class="cover__number">1<"#))
|
||||
#expect(html.contains(#"aria-hidden="true""#))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `sets a volume marker smaller than an issue number`() {
|
||||
let html = Cover(.init(tone: "plum", title: "HOLLOW\nCOMET", imprint: "SŌKAI BOOKS", number: "VOL 4")).render()
|
||||
|
||||
#expect(html.contains(#"class="cover__number cover__number--text">VOL 4<"#))
|
||||
}
|
||||
|
||||
// MARK: Chip tests
|
||||
|
||||
@Test
|
||||
func `checks a certified chip`() {
|
||||
#expect(Chip("CGC 9.8", tone: .good, isCertified: true).render().hasPrefix(#"<span class="chip chip--good"><svg"#))
|
||||
#expect(Chip("Raw · NM").render() == #"<span class="chip">Raw · NM</span>"#)
|
||||
}
|
||||
|
||||
// MARK: ListingCard tests
|
||||
|
||||
@Test
|
||||
func `links a card to its listing and gates saving behind the sign-in`() {
|
||||
let html = ListingCard(.stub()).render()
|
||||
|
||||
#expect(html.contains(#"href="/listing/nightward-1">Nightward #1</a>"#))
|
||||
#expect(html.contains(#"href="/sign-in?return=/listing/nightward-1""#))
|
||||
#expect(html.contains("€240"))
|
||||
#expect(html.contains("CGC 9.8"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `names the shop by the card's context`() {
|
||||
#expect(ListingCard(.stub(), context: .browse).render().contains(">Kestrel Row Comics</a>"))
|
||||
#expect(ListingCard(.stub(), context: .search).render().contains(">Kestrel Row Comics · NL · ship €6</a>"))
|
||||
#expect(!ListingCard(.stub(), context: .shop).render().contains("/shop/kestrel-row-comics"))
|
||||
}
|
||||
|
||||
// MARK: Pagination tests
|
||||
|
||||
@Test
|
||||
func `renders no page links for a single page`() {
|
||||
#expect(Pagination(current: 1, total: 1) { "/?page=\($0)" }.render().isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `marks the current page and offers the next until the last`() {
|
||||
let middle = Pagination(current: 2, total: 3) { "/?page=\($0)" }.render()
|
||||
let last = Pagination(current: 3, total: 3) { "/?page=\($0)" }.render()
|
||||
|
||||
#expect(middle.contains(#"href="/?page=2" aria-current="page""#))
|
||||
#expect(middle.contains(#"href="/?page=3">Next</a>"#))
|
||||
#expect(!last.contains("Next"))
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
[1, 2, 3, 9, -1],
|
||||
[[1, 2], [3, 4], [5], [5], [1, 2]]
|
||||
))
|
||||
func `slices a page out of a list, clamping the page`(
|
||||
page: Int,
|
||||
expects items: [Int]
|
||||
) {
|
||||
let slice = Pagination.slice([1, 2, 3, 4, 5], page: page, size: 2)
|
||||
|
||||
#expect(slice.items == items)
|
||||
#expect(slice.total == 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `counts one page for an empty list`() {
|
||||
let slice = Pagination.slice([Int](), page: 1, size: 12)
|
||||
|
||||
#expect(slice.items.isEmpty)
|
||||
#expect(slice.current == 1)
|
||||
#expect(slice.total == 1)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import Persistence
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"Listing+Display extension",
|
||||
.tags(.extensionTests)
|
||||
)
|
||||
struct ListingDisplayTests {
|
||||
|
||||
// MARK: Computed tests
|
||||
|
||||
@Test(arguments: zip(
|
||||
[9.8, 9.0, 8.5, 7.0, 6.5],
|
||||
[Chip.Tone.good, .good, .neutral, .neutral, .warn]
|
||||
))
|
||||
func `tones a slab's chip by its grade`(
|
||||
grade: Double,
|
||||
expects tone: Chip.Tone
|
||||
) {
|
||||
let chip = Listing.stub(grade: grade).chip
|
||||
|
||||
#expect(chip.label == "CGC \(grade)")
|
||||
#expect(chip.tone == tone)
|
||||
#expect(chip.isCertified)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `marks a raw issue's chip as seller-graded`() {
|
||||
let chip = Listing.stub(grader: nil, grade: 9.4, condition: "NM").chip
|
||||
|
||||
#expect(chip.label == "Raw · NM")
|
||||
#expect(chip.tone == .neutral)
|
||||
#expect(!chip.isCertified)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `states a collected edition's condition alone`() {
|
||||
#expect(Listing.stub(format: "manga", grader: nil, grade: nil, condition: "Like new").chip.label == "Like new")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `leads a collected edition's summary with its format`() {
|
||||
#expect(Listing.stub().summary == "Meridian Comics · 2019 · 1st print")
|
||||
#expect(Listing.stub(format: "hardcovers").summary == "Hardcover · Meridian Comics · 2019 · 1st print")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `derives the paths from the slugs`() {
|
||||
#expect(Listing.stub().path == "/listing/nightward-1")
|
||||
#expect(Shop.stub().path == "/shop/kestrel-row-comics")
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
["Kestrel Row Comics", "Inkwell & Staple", "Silverfish"],
|
||||
["KR", "IS", "S"]
|
||||
))
|
||||
func `monogram`(
|
||||
of name: String,
|
||||
expects monogram: String
|
||||
) {
|
||||
#expect(Shop.stub(name: name).monogram == monogram)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
[24_000, 1_250, 5, 0],
|
||||
["€240", "€12.50", "€0.05", "€0"]
|
||||
))
|
||||
func `euros`(
|
||||
of cents: Int,
|
||||
expects price: String
|
||||
) {
|
||||
#expect(cents.euros == price)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `returns to the gated action's path after sign-in`() {
|
||||
#expect(String.Route.signIn(returningTo: "/shop/a?keys=1&page=2") == "/sign-in?return=/shop/a%3Fkeys%3D1%26page%3D2")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import Elementary
|
||||
import Persistence
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"Catalogue pages",
|
||||
.tags(.page)
|
||||
)
|
||||
struct CataloguePagesTests {
|
||||
|
||||
// MARK: IndexPage tests
|
||||
|
||||
@Test
|
||||
func `landing page fills its rows and fans its covers from the listings`() {
|
||||
let singles = (1...6).map { Listing.stub(slug: "single-\($0)", title: "Single \($0)") }
|
||||
let manga = Listing.stub(slug: "manga-1", title: "Manga 1", format: "manga")
|
||||
let html = IndexPage(
|
||||
locale: .init(identifier: "en"),
|
||||
listings: .init(from: singles + [manga])
|
||||
).render()
|
||||
|
||||
#expect(html.contains("New in single issues"))
|
||||
#expect(html.contains("Single 5"))
|
||||
#expect(!html.contains("Single 6"))
|
||||
#expect(html.contains("Manga 1"))
|
||||
#expect(html.contains("hero__covers"))
|
||||
#expect(html.contains(#"href="/search?format=manga&format=trade-paperbacks&format=hardcovers""#))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `landing page drops its rows without listings`() {
|
||||
let html = IndexPage(locale: .init(identifier: "en")).render()
|
||||
|
||||
#expect(!html.contains("New in single issues"))
|
||||
#expect(!html.contains("hero__covers"))
|
||||
#expect(html.contains("Passkeys, not passwords"))
|
||||
}
|
||||
|
||||
// MARK: SearchPage tests
|
||||
|
||||
@Test
|
||||
func `search page heads the results with the query and ticks the applied filters`() {
|
||||
let html = SearchPage(
|
||||
parameters: .init(query: "q=nightward+1&format=single-issues&publisher=Meridian+Comics"),
|
||||
matches: [.stub(), .stub(slug: "b", shop: .stub(slug: "other", name: "Other"))],
|
||||
publishers: ["Blackline", "Meridian Comics"]
|
||||
).render()
|
||||
|
||||
#expect(html.contains("<title>“nightward 1” — CCN</title>"))
|
||||
#expect(html.contains("2 listings"))
|
||||
#expect(html.contains("from 2 shops"))
|
||||
#expect(html.contains(#"name="q" value="nightward 1""#))
|
||||
#expect(html.contains(#"value="Meridian Comics" checked"#))
|
||||
#expect(!html.contains(#"value="Blackline" checked"#))
|
||||
#expect(html.contains(#"aria-label="Remove filter: Meridian Comics""#))
|
||||
#expect(html.contains("/css/search.css"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `search page pages through more matches than fit`() {
|
||||
let matches = (1...13).map { Listing.stub(slug: "l-\($0)", title: "Listing \($0).") }
|
||||
let html = SearchPage(
|
||||
parameters: .init(query: "page=2"),
|
||||
matches: matches,
|
||||
publishers: []
|
||||
).render()
|
||||
|
||||
#expect(html.contains("13 listings"))
|
||||
#expect(html.contains("Listing 13."))
|
||||
#expect(!html.contains("Listing 12."))
|
||||
#expect(html.contains(#"href="/search?page=2" aria-current="page""#))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `search page offers a way out of an empty result`() {
|
||||
let html = SearchPage(
|
||||
parameters: .init(query: "q=nothing"),
|
||||
matches: [],
|
||||
publishers: []
|
||||
).render()
|
||||
|
||||
#expect(html.contains("No listings match"))
|
||||
#expect(html.contains(#"href="/search?ships-from=anywhere""#))
|
||||
}
|
||||
|
||||
// MARK: ListingPage tests
|
||||
|
||||
@Test
|
||||
func `listing page states the copy and gates every action behind the sign-in`() {
|
||||
let html = ListingPage(
|
||||
listing: .stub(keyNotes: "First appearance"),
|
||||
related: [.stub(slug: "nightward-2", title: "Nightward #2")]
|
||||
).render()
|
||||
|
||||
#expect(html.contains("<title>Nightward #1 · CGC 9.8 — CCN</title>"))
|
||||
#expect(html.contains("+ €6 tracked shipping to NL · accepts offers"))
|
||||
#expect(html.contains("CGC 9.8 · cert 4312087-002"))
|
||||
#expect(html.contains("First appearance"))
|
||||
#expect(html.contains("More from Kestrel Row Comics"))
|
||||
#expect(html.contains("Nightward #2"))
|
||||
|
||||
for action in ["Buy now", "Make an offer", "Message seller", "Save"] {
|
||||
#expect(html.contains(action))
|
||||
}
|
||||
|
||||
#expect(html.components(separatedBy: #"href="/sign-in?return=/listing/nightward-1""#).count - 1 >= 5)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `listing page leaves out what the listing does not offer or state`() {
|
||||
let html = ListingPage(
|
||||
listing: .stub(grader: nil, grade: nil, condition: "Sealed", acceptsOffers: false),
|
||||
related: []
|
||||
).render()
|
||||
|
||||
#expect(!html.contains("Make an offer"))
|
||||
#expect(!html.contains("Key notes"))
|
||||
#expect(!html.contains("More from"))
|
||||
#expect(html.contains("<dt>Condition</dt>"))
|
||||
}
|
||||
|
||||
// MARK: ShopPage tests
|
||||
|
||||
@Test
|
||||
func `shop page counts its tabs from the whole inventory and lists the matches`() {
|
||||
let inventory = [
|
||||
Listing.stub(),
|
||||
.stub(slug: "b", title: "Second"),
|
||||
.stub(slug: "c", title: "A manga", format: "manga"),
|
||||
]
|
||||
let html = ShopPage(
|
||||
shop: .stub(),
|
||||
inventory: inventory,
|
||||
matches: [inventory[2]],
|
||||
parameters: .init(query: "format=manga&keys=1")
|
||||
).render()
|
||||
|
||||
#expect(html.contains("<title>Kestrel Row Comics — CCN</title>"))
|
||||
#expect(html.contains("All · 3"))
|
||||
#expect(html.contains("Single issues · 2"))
|
||||
#expect(html.contains(#"href="/shop/kestrel-row-comics?keys=1&format=manga" aria-current="page""#))
|
||||
#expect(!html.contains("Hardcovers ·"))
|
||||
#expect(html.contains("A manga"))
|
||||
#expect(!html.contains("Second"))
|
||||
#expect(html.contains(#"href="/shop/kestrel-row-comics?format=manga" aria-pressed="true""#))
|
||||
#expect(html.contains(#"type="hidden" name="format" value="manga""#))
|
||||
}
|
||||
|
||||
// MARK: Shared tests
|
||||
|
||||
@Test
|
||||
func `no catalogue page renders an inline style`() {
|
||||
let pages = [
|
||||
SearchPage(parameters: .init(), matches: [.stub()], publishers: ["Blackline"]).render(),
|
||||
ListingPage(listing: .stub(), related: [.stub()]).render(),
|
||||
ShopPage(shop: .stub(), inventory: [.stub()], matches: [.stub()], parameters: .init()).render(),
|
||||
]
|
||||
|
||||
for html in pages {
|
||||
#expect(!html.contains("style="))
|
||||
#expect(!html.contains("<style"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import Persistence
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"SearchParameters type",
|
||||
.tags(.type)
|
||||
)
|
||||
struct SearchParametersTests {
|
||||
|
||||
// MARK: Initializers tests
|
||||
|
||||
@Test
|
||||
func `decodes a form-encoded query and drops its empty fields`() {
|
||||
let parameters = SearchParameters(query: "q=nightward+1&publisher=Harbour+%26+Vale&price-min=&grade-max=+")
|
||||
|
||||
#expect(parameters.items == [
|
||||
.init(name: "q", value: "nightward 1"),
|
||||
.init(name: "publisher", value: "Harbour & Vale"),
|
||||
])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `has no parameters without a query`() {
|
||||
#expect(SearchParameters(query: nil).items.isEmpty)
|
||||
#expect(SearchParameters(query: nil).queryString.isEmpty)
|
||||
}
|
||||
|
||||
// MARK: Computed tests
|
||||
|
||||
@Test
|
||||
func `reads every criterion into the filter`() {
|
||||
let query = "q=saint&format=manga&format=hardcovers&grading=slabbed&grade-min=6.0&grade-max=9.8&first-print=1"
|
||||
+ "&keys=1&publisher=Blackline&price-min=10&price-max=50&ships-from=nl&offers=1&verified=1&sort=price-asc"
|
||||
|
||||
#expect(SearchParameters(query: query).filter == .init(
|
||||
query: "saint",
|
||||
formats: ["manga", "hardcovers"],
|
||||
isSlabbed: true,
|
||||
gradeMin: 6,
|
||||
gradeMax: 9.8,
|
||||
firstPrintOnly: true,
|
||||
keysOnly: true,
|
||||
publishers: ["Blackline"],
|
||||
priceMin: 1_000,
|
||||
priceMax: 5_000,
|
||||
shipsFrom: .nl,
|
||||
acceptsOffers: true,
|
||||
verifiedShops: true,
|
||||
sort: .priceAscending
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `ships from the EU, newest first, by default`() {
|
||||
#expect(SearchParameters().filter == .init(shipsFrom: .eu))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `matches slabbed and raw alike when both are ticked`() {
|
||||
#expect(SearchParameters(query: "grading=slabbed&grading=raw").filter.isSlabbed == nil)
|
||||
#expect(SearchParameters(query: "grading=raw").filter.isSlabbed == false)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `ignores values it cannot read`() {
|
||||
let filter = SearchParameters(query: "grade-min=high&price-max=cheap&ships-from=mars&sort=random&page=-3").filter
|
||||
|
||||
#expect(filter == .init(shipsFrom: .eu))
|
||||
#expect(SearchParameters(query: "page=-3").page == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `offers a pill per active criterion, each linking to the search without it`() {
|
||||
let parameters = SearchParameters(query: "format=manga&grade-min=6&price-max=50&ships-from=anywhere&page=2")
|
||||
let pills = parameters.pills
|
||||
|
||||
#expect(pills.map(\.label) == ["Manga", "Grade 6.0–10.0", "Up to €50"])
|
||||
#expect(pills[0].removed.queryString == "?grade-min=6&price-max=50&ships-from=anywhere")
|
||||
#expect(pills[1].removed.queryString == "?format=manga&price-max=50&ships-from=anywhere")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `removes the default region by naming every region instead`() {
|
||||
let pill = SearchParameters().pills.first
|
||||
|
||||
#expect(pill?.label == "Ships from EU")
|
||||
#expect(pill?.removed.queryString == "?ships-from=anywhere")
|
||||
}
|
||||
|
||||
// MARK: Methods tests
|
||||
|
||||
@Test
|
||||
func `keeps the other parameters when turning the page, and returns to the first when changing them`() {
|
||||
let parameters = SearchParameters(query: "q=saint&page=2")
|
||||
|
||||
#expect(parameters.setting(.page, to: "3").queryString == "?q=saint&page=3")
|
||||
#expect(parameters.setting(.sort, to: "price-asc").queryString == "?q=saint&sort=price-asc")
|
||||
#expect(parameters.toggling(.keys, value: "1").toggling(.keys, value: "1").queryString == "?q=saint")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `escapes what it writes back into a path`() {
|
||||
let parameters = SearchParameters().setting(.publisher, to: "Harbour & Vale")
|
||||
|
||||
#expect(parameters.path("/search") == "/search?publisher=Harbour%20%26%20Vale")
|
||||
#expect(SearchParameters(query: String(parameters.queryString.dropFirst())) == parameters)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `narrows a shop's filter to the shop, wherever it ships from`() {
|
||||
let filter = SearchParameters(query: "keys=1").filter(inShop: "longbox-atlantic")
|
||||
|
||||
#expect(filter == .init(keysOnly: true, shipsFrom: .anywhere, shop: "longbox-atlantic"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -102,9 +102,12 @@ private extension StaticFileTests {
|
||||
(.icon192, ["icon-192.png"]),
|
||||
(.icon512, ["icon-512.png"]),
|
||||
(.index, ["css/index.css", "js/index.js"]),
|
||||
(.listing, ["css/listing.css"]),
|
||||
(.notFound, ["css/not-found.css", "js/not-found.js"]),
|
||||
(.robots, ["robots.txt"]),
|
||||
(.search, ["css/search.css"]),
|
||||
(.shared, ["css/shared.css", "js/shared.js"]),
|
||||
(.shop, ["css/shop.css"]),
|
||||
(.site, ["site.webmanifest"]),
|
||||
(.sitemap, ["sitemap.xml"]),
|
||||
]
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import Hummingbird
|
||||
import HummingbirdTesting
|
||||
import Logging
|
||||
import Persistence
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"CatalogueController controller",
|
||||
.tags(.controller)
|
||||
)
|
||||
struct CatalogueControllerTests {
|
||||
|
||||
// MARK: Functional tests
|
||||
|
||||
@Test
|
||||
func `serves the search results narrowed by the query parameters`() async throws {
|
||||
try await withApp { client in
|
||||
try await client.execute(
|
||||
uri: "/search?q=nightward+1&ships-from=anywhere&sort=price-asc",
|
||||
method: .get
|
||||
) { response in
|
||||
let body = String(buffer: response.body)
|
||||
|
||||
#expect(response.status == .ok)
|
||||
#expect(response.headers[.contentType] == "text/html; charset=utf-8")
|
||||
#expect(body.contains("4 listings"))
|
||||
#expect(body.contains("from 4 shops"))
|
||||
#expect(!body.contains("Ferrous Saint"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `keeps listings from outside the EU out of the default search`() async throws {
|
||||
try await withApp { client in
|
||||
try await client.execute(
|
||||
uri: "/search",
|
||||
method: .get
|
||||
) { response in
|
||||
let body = String(buffer: response.body)
|
||||
|
||||
#expect(body.contains("17 listings"))
|
||||
#expect(!body.contains("Longbox Atlantic"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `serves a listing with more from its shop`() async throws {
|
||||
try await withApp { client in
|
||||
try await client.execute(
|
||||
uri: "/listing/nightward-1-cgc-9-8",
|
||||
method: .get
|
||||
) { response in
|
||||
let body = String(buffer: response.body)
|
||||
|
||||
#expect(response.status == .ok)
|
||||
#expect(body.contains("cert 4312087-002"))
|
||||
#expect(body.contains("More from Kestrel Row Comics"))
|
||||
#expect(body.contains("Echo Country #3"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `serves a shop wherever it ships from, narrowed by its quick filters`() async throws {
|
||||
try await withApp { client in
|
||||
try await client.execute(
|
||||
uri: "/shop/longbox-atlantic?grading=slabbed",
|
||||
method: .get
|
||||
) { response in
|
||||
let body = String(buffer: response.body)
|
||||
|
||||
#expect(response.status == .ok)
|
||||
#expect(body.contains("All · 3"))
|
||||
#expect(body.contains("Vermilion Lake #7"))
|
||||
#expect(!body.contains("Solar Divide, Vol. 1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: ["/listing/missing", "/shop/missing"])
|
||||
func `does not find an unknown slug`(
|
||||
uri: String
|
||||
) async throws {
|
||||
try await withApp { client in
|
||||
try await client.execute(
|
||||
uri: uri,
|
||||
method: .get
|
||||
) { response in
|
||||
#expect(response.status == .notFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension CatalogueControllerTests {
|
||||
|
||||
/// Runs the body against the controller's routes over a migrated, seeded in-memory catalogue, shutting the pool down after.
|
||||
func withApp(
|
||||
_ body: @escaping @Sendable (any TestClientProtocol) async throws -> Void
|
||||
) async throws {
|
||||
let fluent = try Service(
|
||||
driver: .inMemory,
|
||||
logger: Logger(label: "test")
|
||||
)()
|
||||
|
||||
do {
|
||||
await PrepareDB()(for: fluent)
|
||||
|
||||
try await fluent.migrate()
|
||||
|
||||
let router = Router(context: WebsiteRequestContext.self)
|
||||
|
||||
router.addRoutes(CatalogueController<WebsiteRequestContext>(repository: .init(fluent: fluent)).routes)
|
||||
|
||||
try await Application(router: router).test(.router, body)
|
||||
} catch {
|
||||
try? await fluent.shutdown()
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
try await fluent.shutdown()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import Foundation
|
||||
import Persistence
|
||||
|
||||
extension Listing {
|
||||
/// A listing for tests: a slabbed single issue unless told otherwise.
|
||||
static func stub(
|
||||
slug: String = "nightward-1",
|
||||
shop: Shop = .stub(),
|
||||
title: String = "Nightward #1",
|
||||
format: String = "single-issues",
|
||||
grader: String? = "CGC",
|
||||
grade: Double? = 9.8,
|
||||
condition: String = "NM/M",
|
||||
keyNotes: String? = nil,
|
||||
priceCents: Int = 24_000,
|
||||
acceptsOffers: Bool = true
|
||||
) -> Self {
|
||||
.init(
|
||||
slug: slug, shop: shop, title: title, seriesTitle: "Nightward (2019 series)", issueLabel: "#1",
|
||||
publisher: "Meridian Comics", year: 2019, format: format, printing: "1st print",
|
||||
printingDetail: "First printing", isFirstPrint: true, grader: grader, grade: grade,
|
||||
condition: condition, certNumber: grader == nil ? nil : "4312087-002", pageQuality: "White",
|
||||
keyNotes: keyNotes, sellerNotes: "Clean case.", priceCents: priceCents, shippingCents: 600,
|
||||
acceptsOffers: acceptsOffers,
|
||||
cover: .init(tone: "navy", title: "NIGHT\nWARD", imprint: "MERIDIAN", number: "1"),
|
||||
listedAt: .init(timeIntervalSince1970: 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Shop {
|
||||
/// A shop for tests: a verified Amsterdam seller unless told otherwise.
|
||||
static func stub(
|
||||
slug: String = "kestrel-row-comics",
|
||||
name: String = "Kestrel Row Comics",
|
||||
isVerified: Bool = true
|
||||
) -> Self {
|
||||
.init(
|
||||
slug: slug, name: name, city: "Amsterdam", country: "NL", sinceYear: 2023, isVerified: isVerified,
|
||||
rating: 4.9, ratedSales: 312, soldCount: 1108, handlingDays: 2, shipsNote: "ships worldwide, tracked",
|
||||
returnsNote: "14-day returns", about: "Modern keys."
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user