Files
Kevin L'Hereec 4174d92711
Build & Deploy / build (push) Failing after 4s
Build & Deploy / docker (push) Skipped
feat: track purchase site URL for new components
- New nullable purchase_url column on components
- Store/update validators accept an optional purchaseUrl (must be a valid
  absolute URL with a protocol when provided)
- components/create.vue: NewPartSearch import now also fills purchaseUrl
  from the scraped part's source URL (pc-builder.io product page); the
  field remains manually editable for second-hand parts entered by hand
- components/edit.vue: purchaseUrl is editable after creation
- components/show.vue: displays the purchase site as a clickable link
  when present
- 4 new functional tests: create with/without a purchase URL, invalid URL
  rejected (asserts the Inertia inputErrorsBag flash, not a raw 422 -
  content negotiation only returns JSON for XHR/API-style requests), and
  updating an existing component's purchase URL
2026-07-23 14:10:54 +02:00

199 lines
6.2 KiB
TypeScript

import { test } from '@japa/runner'
import { DateTime } from 'luxon'
import User from '#models/user'
import Component from '#models/component'
import ComponentType from '#models/component_type'
async function createUser(email: string) {
return User.create({
name: 'Test User',
email,
password: 'secret@123A',
})
}
async function createType(name: string, unique = true) {
return ComponentType.firstOrCreate({ name }, { name, unique })
}
async function createComponent(userId: number, typeId: number) {
return Component.create({
name: 'Test Component',
price: 20,
typeId,
userId,
computerId: null,
purchaseDate: DateTime.now(),
})
}
test.group('Components authorization (IDOR)', () => {
test('owner can update their own component', async ({ client, assert }) => {
const owner = await createUser('owner-comp-update@example.com')
const type = await createType('RAM-comp-1')
const component = await createComponent(owner.id, type.id)
const response = await client
.put(`/components/${component.id}`)
.loginAs(owner)
.withCsrfToken()
.redirects(0)
.json({
name: 'Updated name',
price: 30,
typeId: type.id,
purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'),
})
response.assertStatus(302)
await component.refresh()
assert.equal(component.name, 'Updated name')
})
test('a user cannot update another user component', async ({ client, assert }) => {
const owner = await createUser('owner-comp-a@example.com')
const attacker = await createUser('attacker-comp-a@example.com')
const type = await createType('RAM-comp-2')
const component = await createComponent(owner.id, type.id)
const response = await client
.put(`/components/${component.id}`)
.loginAs(attacker)
.withCsrfToken()
.redirects(0)
.json({
name: 'Hacked name',
price: 1,
typeId: type.id,
purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'),
})
response.assertStatus(404)
await component.refresh()
assert.notEqual(component.name, 'Hacked name')
})
test('a user cannot delete another user component', async ({ client, assert }) => {
const owner = await createUser('owner-comp-b@example.com')
const attacker = await createUser('attacker-comp-b@example.com')
const type = await createType('RAM-comp-3')
const component = await createComponent(owner.id, type.id)
const response = await client
.delete(`/components/${component.id}`)
.loginAs(attacker)
.withCsrfToken()
.redirects(0)
response.assertStatus(404)
const stillExists = await Component.find(component.id)
assert.isNotNull(stillExists)
})
test('an unauthenticated request cannot delete a component', async ({ client, assert }) => {
const owner = await createUser('owner-comp-c@example.com')
const type = await createType('RAM-comp-4')
const component = await createComponent(owner.id, type.id)
const response = await client.delete(`/components/${component.id}`)
response.assertRedirectsTo('/auth/login')
const stillExists = await Component.find(component.id)
assert.isNotNull(stillExists)
})
})
test.group('Components purchase URL', () => {
test('a component can be created with a purchase site URL', async ({ client, assert }) => {
const user = await createUser('purchase-url-a@example.com')
const type = await createType('RAM-purchase-1')
const response = await client
.post('/components')
.loginAs(user)
.withCsrfToken()
.redirects(0)
.json({
name: 'New GPU',
price: 599.99,
typeId: type.id,
purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'),
purchaseUrl: 'https://fr.pc-builder.io/gpu/example-p1',
quantity: 1,
})
response.assertStatus(302)
const component = await Component.query().where('userId', user.id).firstOrFail()
assert.equal(component.purchaseUrl, 'https://fr.pc-builder.io/gpu/example-p1')
})
test('a component can be created without a purchase site URL', async ({ client, assert }) => {
const user = await createUser('purchase-url-b@example.com')
const type = await createType('RAM-purchase-2')
const response = await client
.post('/components')
.loginAs(user)
.withCsrfToken()
.redirects(0)
.json({
name: 'Used RAM stick',
price: 20,
typeId: type.id,
purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'),
quantity: 1,
})
response.assertStatus(302)
const component = await Component.query().where('userId', user.id).firstOrFail()
assert.isNull(component.purchaseUrl)
})
test('an invalid purchase URL is rejected', async ({ client, assert }) => {
const user = await createUser('purchase-url-c@example.com')
const type = await createType('RAM-purchase-3')
const response = await client
.post('/components')
.loginAs(user)
.withCsrfToken()
.redirects(0)
.json({
name: 'Suspicious component',
price: 20,
typeId: type.id,
purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'),
purchaseUrl: 'not-a-url',
quantity: 1,
})
response.assertStatus(302)
assert.property(response.flashMessage('inputErrorsBag'), 'purchaseUrl')
const created = await Component.query().where('userId', user.id).first()
assert.isNull(created)
})
test('the purchase URL can be updated on an existing component', async ({ client, assert }) => {
const owner = await createUser('purchase-url-d@example.com')
const type = await createType('RAM-purchase-4')
const component = await createComponent(owner.id, type.id)
const response = await client
.put(`/components/${component.id}`)
.loginAs(owner)
.withCsrfToken()
.redirects(0)
.json({
name: component.name,
price: component.price,
typeId: type.id,
purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'),
purchaseUrl: 'https://fr.pc-builder.io/cpu/example-p2',
})
response.assertStatus(302)
await component.refresh()
assert.equal(component.purchaseUrl, 'https://fr.pc-builder.io/cpu/example-p2')
})
})