Explorar o código

complete unit tests coverage

Olivier Massot hai 1 ano
pai
achega
5fda4348e9

+ 38 - 26
services/data/Filters/SearchFilter.ts

@@ -1,4 +1,5 @@
 import type { Query as PiniaOrmQuery } from 'pinia-orm'
+import type { Ref } from 'vue'
 import type { ApiFilter } from '~/types/data'
 import ApiResource from '~/models/ApiResource'
 import { SEARCH_STRATEGY } from '~/types/enum/data'
@@ -7,7 +8,7 @@ import RefUtils from '~/services/utils/refUtils'
 
 export default class SearchFilter extends AbstractFilter implements ApiFilter {
   field: string
-  filterValue: Ref<string>
+  filterValue: Ref<string | null>
   mode: SEARCH_STRATEGY
 
   /**
@@ -20,7 +21,7 @@ export default class SearchFilter extends AbstractFilter implements ApiFilter {
    */
   constructor(
     field: string,
-    value: Ref<string>,
+    value: Ref<string | null>,
     mode: SEARCH_STRATEGY = SEARCH_STRATEGY.EXACT,
     reactiveFilter: boolean = false,
   ) {
@@ -30,6 +31,38 @@ export default class SearchFilter extends AbstractFilter implements ApiFilter {
     this.mode = mode
   }
 
+  protected search(value: string, filterValue: Ref<string | null>): boolean {
+    if (filterValue.value === null) {
+      return false
+    }
+
+    let wordStartRx: RegExp | null = null
+    if (this.mode === SEARCH_STRATEGY.WORD_START) {
+      wordStartRx = new RegExp(
+        `^${filterValue.value}|\\s${filterValue.value}`,
+        'i',
+      )
+    }
+
+    if (this.mode === SEARCH_STRATEGY.EXACT) {
+      return value === filterValue.value
+    } else if (this.mode === SEARCH_STRATEGY.IEXACT) {
+      return value.toLowerCase() === filterValue.value.toLowerCase()
+    } else if (this.mode === SEARCH_STRATEGY.PARTIAL) {
+      return value.includes(filterValue.value)
+    } else if (this.mode === SEARCH_STRATEGY.IPARTIAL) {
+      return value.toLowerCase().includes(filterValue.value.toLowerCase())
+    } else if (this.mode === SEARCH_STRATEGY.START) {
+      return value.startsWith(filterValue.value)
+    } else if (this.mode === SEARCH_STRATEGY.END) {
+      return value.endsWith(filterValue.value)
+    } else if (this.mode === SEARCH_STRATEGY.WORD_START) {
+      return wordStartRx!.test(value)
+    } else {
+      throw new Error('Unrecognized mode')
+    }
+  }
+
   public applyToPiniaOrmQuery(
     query: PiniaOrmQuery<ApiResource>,
   ): PiniaOrmQuery<ApiResource> {
@@ -42,30 +75,9 @@ export default class SearchFilter extends AbstractFilter implements ApiFilter {
       return query
     }
 
-    let wordStartRx: RegExp | null = null
-    if (this.mode === SEARCH_STRATEGY.WORD_START) {
-      wordStartRx = new RegExp(`^${filterValue.value}|\\s${filterValue.value}`)
-    }
-
-    return query.where(this.field, (value: string) => {
-      if (this.mode === SEARCH_STRATEGY.EXACT) {
-        return value === filterValue.value
-      } else if (this.mode === SEARCH_STRATEGY.IEXACT) {
-        return value.toLowerCase() === filterValue.value.toLowerCase()
-      } else if (this.mode === SEARCH_STRATEGY.PARTIAL) {
-        return value.includes(filterValue.value)
-      } else if (this.mode === SEARCH_STRATEGY.IPARTIAL) {
-        return value.toLowerCase().includes(filterValue.value.toLowerCase())
-      } else if (this.mode === SEARCH_STRATEGY.START) {
-        return value.startsWith(filterValue.value)
-      } else if (this.mode === SEARCH_STRATEGY.END) {
-        return value.endsWith(filterValue.value)
-      } else if (this.mode === SEARCH_STRATEGY.WORD_START) {
-        return wordStartRx!.test(value)
-      } else {
-        throw new Error('Unrecognized mode')
-      }
-    })
+    return query.where(this.field, (value: string) =>
+      this.search(value, filterValue),
+    )
   }
 
   public getApiQueryPart(): string {

+ 1 - 0
services/data/entityManager.ts

@@ -56,6 +56,7 @@ class EntityManager {
    * @param model
    */
   public getQuery(model: typeof ApiResource): PiniaOrmQuery<ApiResource> {
+    // TODO: quid des uuid?
     return this.getRepository(model).where((val) => Number.isInteger(val.id))
   }
 

+ 2 - 1
services/utils/refUtils.ts

@@ -1,9 +1,10 @@
 import type { UnwrapRef } from 'vue'
+import { ref, isRef } from 'vue'
 
 export default class RefUtils {
   /**
    * Convertit la valeur du filtre en référence. S'il s'agit déjà d'une ref,
-   * selon que `maintainReactivity` soit vrai ou faux, on conserve la référence existante
+   * selon que `maintainReactivity` est vrai ou faux, on conserve la référence existante
    * ou bien on la recréé pour briser la réactivité.
    *
    * @param value

+ 38 - 3
tests/units/services/data/entityManager.test.ts

@@ -2,10 +2,20 @@ import { describe, test, vi, expect, beforeEach, afterEach } from 'vitest'
 import { Repository } from 'pinia-orm'
 import type { Element } from 'pinia-orm'
 import { Str, Uid } from 'pinia-orm/dist/decorators'
-import EntityManager from '~/services/data/entityManager'
 import ApiResource from '~/models/ApiResource'
 import ApiModel from '~/models/ApiModel'
 import ApiRequestService from '~/services/data/apiRequestService'
+import EntityManager from '~/services/data/entityManager';
+
+
+class TestableEntityManager extends EntityManager {
+  public removeTempAfterPersist(
+    model: typeof ApiResource,
+    tempInstanceId: number | string,
+  ): void {
+    return super.removeTempAfterPersist(model, tempInstanceId)
+  }
+}
 
 class DummyApiResource extends ApiResource {
   static entity = 'dummyResource'
@@ -46,7 +56,7 @@ vi.mock('~/models/models', async () => {
 })
 
 let apiRequestService: ApiRequestService
-let entityManager: EntityManager
+let entityManager: TestableEntityManager
 let repo: Repository<ApiResource>
 let _getRepo: (model: typeof ApiResource) => Repository<ApiResource>
 
@@ -58,7 +68,7 @@ beforeEach(() => {
   apiRequestService = vi.fn() as ApiRequestService
   _getRepo = vi.fn((model: typeof ApiResource) => repo)
 
-  entityManager = new EntityManager(apiRequestService, _getRepo)
+  entityManager = new TestableEntityManager(apiRequestService, _getRepo)
 })
 
 afterEach(() => {
@@ -76,6 +86,31 @@ describe('getRepository', () => {
   })
 })
 
+describe('getQuery', () => {
+  test('simple call', () => {
+    // @ts-ignore
+    const repo = vi.fn()
+
+    // @ts-ignore
+    entityManager.getRepository = vi.fn(() => repo)
+
+    const query = vi.fn()
+
+    // @ts-ignore
+    repo.where = vi.fn((method) => {
+      // Query method is supposed to exclude NaN ids values
+      expect(method({ id: 123 })).toBeTruthy()
+      expect(method({ id: 'abc' })).toBeFalsy()
+
+      return query
+    })
+
+    const result = entityManager.getQuery(DummyApiResource)
+
+    expect(result).toEqual(query)
+  })
+})
+
 describe('cast', () => {
   test('simple cast', () => {
     // @ts-ignore

+ 73 - 0
tests/units/services/data/filters/equalFilter.test.ts

@@ -0,0 +1,73 @@
+import { describe, expect, test, vi } from 'vitest'
+import type { Query as PiniaOrmQuery } from 'pinia-orm'
+import EqualFilter from '~/services/data/Filters/EqualFilter'
+import type ApiResource from '~/models/ApiResource'
+
+describe('constructor', () => {
+  test('simple call', () => {
+    const filter = new EqualFilter('foo', 123)
+
+    expect(filter.field).toEqual('foo')
+    expect(filter.filterValue).toEqual(123)
+    expect(filter.reactiveFilter).toEqual(false)
+  })
+
+  test('simple call with reactive filters', () => {
+    const filter = new EqualFilter('foo', 123, true)
+
+    expect(filter.field).toEqual('foo')
+    expect(filter.filterValue).toEqual(123)
+    expect(filter.reactiveFilter).toEqual(true)
+  })
+})
+
+describe('applyToPiniaOrmQuery', () => {
+  test('simple call', () => {
+    const filter = new EqualFilter('foo', 123)
+
+    // @ts-ignore
+    const piniaOrmQuery1 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    // @ts-ignore
+    const piniaOrmQuery2 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    piniaOrmQuery1.where = vi.fn(() => piniaOrmQuery2)
+
+    const result = filter.applyToPiniaOrmQuery(piniaOrmQuery1)
+
+    expect(result).toEqual(piniaOrmQuery2)
+    expect(piniaOrmQuery1.where).toHaveBeenCalledWith('foo', 123)
+  })
+
+  test('empty filter value', () => {
+    const filter = new EqualFilter('foo', null)
+
+    // @ts-ignore
+    const piniaOrmQuery1 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    piniaOrmQuery1.where = vi.fn()
+
+    const result = filter.applyToPiniaOrmQuery(piniaOrmQuery1)
+
+    expect(result).toEqual(piniaOrmQuery1)
+    expect(piniaOrmQuery1.where).toHaveBeenCalledTimes(0)
+  })
+})
+
+describe('getApiQueryPart', () => {
+  test('simple call', () => {
+    const filter = new EqualFilter('foo', 123)
+
+    const result = filter.getApiQueryPart()
+
+    expect(result).toEqual('foo=123')
+  })
+
+  test('empty filter value', () => {
+    const filter = new EqualFilter('foo', null)
+
+    const result = filter.getApiQueryPart()
+
+    expect(result).toEqual('')
+  })
+})

+ 56 - 0
tests/units/services/data/filters/orderBy.test.ts

@@ -0,0 +1,56 @@
+import { describe, expect, test, vi } from 'vitest'
+import type { Query as PiniaOrmQuery } from 'pinia-orm'
+import type ApiResource from '~/models/ApiResource'
+import OrderBy from '~/services/data/Filters/OrderBy'
+import { ORDER_BY_DIRECTION } from '~/types/enum/data'
+
+describe('constructor', () => {
+  test('simple call', () => {
+    const filter = new OrderBy('foo')
+
+    expect(filter.field).toEqual('foo')
+  })
+
+  test('simple call with desc direction', () => {
+    const filter = new OrderBy('foo', ORDER_BY_DIRECTION.DESC)
+
+    expect(filter.field).toEqual('foo')
+    expect(filter.mode).toEqual('desc')
+  })
+})
+
+describe('applyToPiniaOrmQuery', () => {
+  test('simple call', () => {
+    const filter = new OrderBy('foo')
+
+    // @ts-ignore
+    const piniaOrmQuery1 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    // @ts-ignore
+    const piniaOrmQuery2 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    piniaOrmQuery1.orderBy = vi.fn(() => piniaOrmQuery2)
+
+    const result = filter.applyToPiniaOrmQuery(piniaOrmQuery1)
+
+    expect(result).toEqual(piniaOrmQuery2)
+  })
+})
+
+describe('getApiQueryPart', () => {
+  test('simple call', () => {
+    const filter = new OrderBy('foo', ORDER_BY_DIRECTION.ASC)
+
+    const result = filter.getApiQueryPart()
+
+    expect(result).toEqual('order[foo]=asc')
+  })
+
+  test('with descendent direction', () => {
+    const filter = new OrderBy('foo', ORDER_BY_DIRECTION.DESC)
+
+    const result = filter.getApiQueryPart()
+
+    expect(result).toEqual('order[foo]=desc')
+  })
+})

+ 68 - 0
tests/units/services/data/filters/pageFilter.test.ts

@@ -0,0 +1,68 @@
+import { describe, expect, test, vi } from 'vitest'
+import type { Query as PiniaOrmQuery } from 'pinia-orm'
+import { ref } from 'vue'
+import type ApiResource from '~/models/ApiResource'
+import PageFilter from '~/services/data/Filters/PageFilter'
+
+describe('constructor', () => {
+  test('simple call', () => {
+    const filter = new PageFilter(ref(1), ref(10))
+
+    expect(filter.page.value).toEqual(1)
+    expect(filter.itemsPerPage.value).toEqual(10)
+  })
+})
+
+describe('applyToPiniaOrmQuery', () => {
+  test('simple call', () => {
+    const filter = new PageFilter(ref(1), ref(10))
+
+    // @ts-ignore
+    const piniaOrmQuery1 = vi.fn() as PiniaOrmQuery<ApiResource>
+    // @ts-ignore
+    const piniaOrmQuery2 = vi.fn() as PiniaOrmQuery<ApiResource>
+    // @ts-ignore
+    const piniaOrmQuery3 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    piniaOrmQuery1.offset = vi.fn(() => piniaOrmQuery2)
+    piniaOrmQuery2.limit = vi.fn(() => piniaOrmQuery3)
+
+    const result = filter.applyToPiniaOrmQuery(piniaOrmQuery1)
+
+    expect(result).toEqual(piniaOrmQuery3)
+
+    expect(piniaOrmQuery1.offset).toHaveBeenCalledWith(0)
+    expect(piniaOrmQuery2.limit).toHaveBeenCalledWith(10)
+  })
+
+  test('with other page number', () => {
+    const filter = new PageFilter(ref(2), ref(20))
+
+    // @ts-ignore
+    const piniaOrmQuery1 = vi.fn() as PiniaOrmQuery<ApiResource>
+    // @ts-ignore
+    const piniaOrmQuery2 = vi.fn() as PiniaOrmQuery<ApiResource>
+    // @ts-ignore
+    const piniaOrmQuery3 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    piniaOrmQuery1.offset = vi.fn(() => piniaOrmQuery2)
+    piniaOrmQuery2.limit = vi.fn(() => piniaOrmQuery3)
+
+    const result = filter.applyToPiniaOrmQuery(piniaOrmQuery1)
+
+    expect(result).toEqual(piniaOrmQuery3)
+
+    expect(piniaOrmQuery1.offset).toHaveBeenCalledWith(20)
+    expect(piniaOrmQuery2.limit).toHaveBeenCalledWith(20)
+  })
+})
+
+describe('getApiQueryPart', () => {
+  test('simple call', () => {
+    const filter = new PageFilter(ref(1), ref(10))
+
+    const result = filter.getApiQueryPart()
+
+    expect(result).toEqual('page=1&itemsPerPage=10')
+  })
+})

+ 208 - 0
tests/units/services/data/filters/searchFilter.test.ts

@@ -0,0 +1,208 @@
+import { describe, expect, test, vi } from 'vitest'
+import type { Query as PiniaOrmQuery } from 'pinia-orm'
+import type { Ref } from 'vue'
+import { ref } from 'vue'
+import EqualFilter from '~/services/data/Filters/EqualFilter'
+import type ApiResource from '~/models/ApiResource'
+import SearchFilter from '~/services/data/Filters/SearchFilter'
+import { SEARCH_STRATEGY } from '~/types/enum/data'
+
+class TestableSearchFilter extends SearchFilter {
+  public search(value: string, filterValue: Ref<string | null>): boolean {
+    return super.search(value, filterValue)
+  }
+}
+
+describe('constructor', () => {
+  test('simple call', () => {
+    const filter = new SearchFilter('foo', ref('abc'))
+
+    expect(filter.field).toEqual('foo')
+    expect(filter.filterValue.value).toEqual('abc')
+    expect(filter.reactiveFilter).toEqual(false)
+  })
+
+  test('simple call with reactive filters and mode', () => {
+    const filter = new SearchFilter(
+      'foo',
+      ref('abc'),
+      SEARCH_STRATEGY.PARTIAL,
+      true,
+    )
+
+    expect(filter.field).toEqual('foo')
+    expect(filter.filterValue.value).toEqual('abc')
+    expect(filter.mode).toEqual(SEARCH_STRATEGY.PARTIAL)
+    expect(filter.reactiveFilter).toEqual(true)
+  })
+})
+
+describe('search', () => {
+  test('exact mode (default)', () => {
+    const filter = new TestableSearchFilter('foo', ref('abc'))
+
+    expect(filter.search('azerty', ref('azerty'))).toBeTruthy()
+    expect(filter.search('azerty', ref('AZERTY'))).toBeFalsy()
+    expect(filter.search('azerty', ref('foo'))).toBeFalsy()
+  })
+
+  test('iexact mode', () => {
+    const filter = new TestableSearchFilter(
+      'foo',
+      ref('abc'),
+      SEARCH_STRATEGY.IEXACT,
+    )
+
+    expect(filter.search('azerty', ref('azerty'))).toBeTruthy()
+    expect(filter.search('azerty', ref('AZERTY'))).toBeTruthy()
+    expect(filter.search('azerty', ref('foo'))).toBeFalsy()
+  })
+
+  test('partial mode', () => {
+    const filter = new TestableSearchFilter(
+      'foo',
+      ref('abc'),
+      SEARCH_STRATEGY.PARTIAL,
+    )
+
+    expect(filter.search('azerty', ref('azerty'))).toBeTruthy()
+    expect(filter.search('azerty', ref('azer'))).toBeTruthy()
+    expect(filter.search('azerty', ref('zer'))).toBeTruthy()
+    expect(filter.search('azerty', ref('AZER'))).toBeFalsy()
+    expect(filter.search('azerty', ref('foo'))).toBeFalsy()
+  })
+
+  test('ipartial mode', () => {
+    const filter = new TestableSearchFilter(
+      'foo',
+      ref('abc'),
+      SEARCH_STRATEGY.IPARTIAL,
+    )
+
+    expect(filter.search('azerty', ref('azerty'))).toBeTruthy()
+    expect(filter.search('azerty', ref('azer'))).toBeTruthy()
+    expect(filter.search('azerty', ref('zer'))).toBeTruthy()
+    expect(filter.search('azerty', ref('AZER'))).toBeTruthy()
+    expect(filter.search('azerty', ref('foo'))).toBeFalsy()
+  })
+
+  test('start mode', () => {
+    const filter = new TestableSearchFilter(
+      'foo',
+      ref('abc'),
+      SEARCH_STRATEGY.START,
+    )
+
+    expect(filter.search('azerty', ref('azerty'))).toBeTruthy()
+    expect(filter.search('azerty', ref('azer'))).toBeTruthy()
+    expect(filter.search('azerty', ref('zer'))).toBeFalsy()
+    expect(filter.search('azerty', ref('AZER'))).toBeFalsy()
+    expect(filter.search('azerty', ref('foo'))).toBeFalsy()
+  })
+
+  test('end mode', () => {
+    const filter = new TestableSearchFilter(
+      'foo',
+      ref('abc'),
+      SEARCH_STRATEGY.END,
+    )
+
+    expect(filter.search('azerty', ref('azerty'))).toBeTruthy()
+    expect(filter.search('azerty', ref('rty'))).toBeTruthy()
+    expect(filter.search('azerty', ref('zer'))).toBeFalsy()
+    expect(filter.search('azerty', ref('AZER'))).toBeFalsy()
+    expect(filter.search('azerty', ref('foo'))).toBeFalsy()
+  })
+
+  test('word-start mode', () => {
+    const filter = new TestableSearchFilter(
+      'foo',
+      ref('abc'),
+      SEARCH_STRATEGY.WORD_START,
+    )
+
+    expect(filter.search('Once upon a time', ref('tim'))).toBeTruthy()
+    expect(filter.search('Once upon a time', ref('onc'))).toBeTruthy()
+    expect(filter.search('Once upon a time', ref('TIM'))).toBeTruthy()
+    expect(filter.search('Once upon a time', ref('ime'))).toBeFalsy()
+    expect(filter.search('Once upon a time', ref('foo'))).toBeFalsy()
+  })
+
+  test('unknown mode', () => {
+    // @ts-ignore
+    const filter = new TestableSearchFilter('foo', ref('abc'), 'other')
+
+    expect(() => filter.search('azerty', ref('azerty'))).toThrowError()
+  })
+
+  test('null filter', () => {
+    // @ts-ignore
+    const filter = new TestableSearchFilter('foo', ref(null))
+
+    expect(filter.search('azerty', ref(null))).toBeFalsy()
+  })
+})
+
+describe('applyToPiniaOrmQuery', () => {
+  test('simple call', () => {
+    const filterValue = ref('abc')
+    const filter = new TestableSearchFilter('foo', filterValue)
+
+    // @ts-ignore
+    const piniaOrmQuery1 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    // @ts-ignore
+    const piniaOrmQuery2 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    piniaOrmQuery1.where = vi.fn((field, callback) => {
+      // eslint-disable-next-line n/no-callback-literal
+      callback('test')
+      return piniaOrmQuery2
+    })
+
+    filter.search = vi.fn((value, filterValue) => true)
+
+    const result = filter.applyToPiniaOrmQuery(piniaOrmQuery1)
+
+    expect(result).toEqual(piniaOrmQuery2)
+    expect(filter.search).toHaveBeenCalledWith('test', filterValue)
+  })
+
+  test('empty filter value', () => {
+    const filterValue = ref(null)
+    const filter = new TestableSearchFilter('foo', filterValue)
+
+    // @ts-ignore
+    const piniaOrmQuery1 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    const result = filter.applyToPiniaOrmQuery(piniaOrmQuery1)
+
+    expect(result).toEqual(piniaOrmQuery1)
+  })
+})
+
+describe('getApiQueryPart', () => {
+  test('simple call', () => {
+    const filter = new TestableSearchFilter('foo', ref('abc'))
+
+    const result = filter.getApiQueryPart()
+
+    expect(result).toEqual('foo[]=abc')
+  })
+
+  test('empty filter value', () => {
+    const filter = new TestableSearchFilter('foo', ref(''))
+
+    const result = filter.getApiQueryPart()
+
+    expect(result).toEqual('')
+  })
+
+  test('null filter value', () => {
+    const filter = new TestableSearchFilter('foo', ref(null))
+
+    const result = filter.getApiQueryPart()
+
+    expect(result).toEqual('')
+  })
+})

+ 104 - 0
tests/units/services/data/query.test.ts

@@ -0,0 +1,104 @@
+import { describe, test, vi, expect, beforeEach, afterEach } from 'vitest'
+import type { Query as PiniaOrmQuery } from 'pinia-orm'
+import { ref } from 'vue'
+import Query from '~/services/data/Query'
+import type { ApiFilter } from '~/types/data'
+import type ApiResource from '~/models/ApiResource'
+import PageFilter from '~/services/data/Filters/PageFilter'
+import EqualFilter from '~/services/data/Filters/EqualFilter'
+import OrderBy from '~/services/data/Filters/OrderBy'
+
+class TestableQuery extends Query {
+  public getFilters() {
+    return this.filters
+  }
+}
+
+describe('constructor', () => {
+  test('simple call', () => {
+    // @ts-ignore
+    const filter1 = vi.fn() as ApiFilter
+    // @ts-ignore
+    const filter2 = vi.fn() as ApiFilter
+
+    const query = new TestableQuery(filter1, filter2)
+
+    expect(query.getFilters()).toEqual([filter1, filter2])
+  })
+})
+
+describe('add', () => {
+  test('simple call', () => {
+    // @ts-ignore
+    const filter1 = vi.fn() as ApiFilter
+    // @ts-ignore
+    const filter2 = vi.fn() as ApiFilter
+
+    const query = new TestableQuery(filter1)
+
+    expect(query.getFilters()).toEqual([filter1])
+
+    const result = query.add(filter2)
+
+    expect(query.getFilters()).toEqual([filter1, filter2])
+
+    expect(result).toEqual(query)
+  })
+})
+
+describe('getUrlQuery', () => {
+  test('simple call', () => {
+    // @ts-ignore
+    const filter1 = vi.fn() as ApiFilter
+    // @ts-ignore
+    const filter2 = vi.fn() as ApiFilter
+    // @ts-ignore
+    const filter3 = vi.fn() as ApiFilter
+
+    const query = new TestableQuery(filter1, filter2, filter3)
+
+    filter1.getApiQueryPart = vi.fn(() => 'foo=1')
+    filter2.getApiQueryPart = vi.fn(() => '')
+    filter3.getApiQueryPart = vi.fn(() => 'bar=2')
+
+    const result = query.getUrlQuery()
+
+    expect(result).toEqual('foo=1&bar=2')
+  })
+})
+
+describe('applyToPiniaOrmQuery', () => {
+  test('simple call', () => {
+    const filter1 = new EqualFilter('a', 1)
+    const filter2 = new EqualFilter('a', 1)
+    const orderBy = new OrderBy('a')
+    const pageFilter = new PageFilter(ref(1), ref(10))
+
+    // @ts-ignore
+    const piniaOrmQuery1 = vi.fn() as PiniaOrmQuery<ApiResource>
+    // @ts-ignore
+    const piniaOrmQuery2 = vi.fn() as PiniaOrmQuery<ApiResource>
+    // @ts-ignore
+    const piniaOrmQuery3 = vi.fn() as PiniaOrmQuery<ApiResource>
+    // @ts-ignore
+    const piniaOrmQuery4 = vi.fn() as PiniaOrmQuery<ApiResource>
+    // @ts-ignore
+    const piniaOrmQuery5 = vi.fn() as PiniaOrmQuery<ApiResource>
+
+    const query = new TestableQuery(orderBy, filter1, pageFilter, filter2)
+
+    filter1.applyToPiniaOrmQuery = vi.fn((query) => piniaOrmQuery2)
+    filter2.applyToPiniaOrmQuery = vi.fn((query) => piniaOrmQuery3)
+    orderBy.applyToPiniaOrmQuery = vi.fn((query) => piniaOrmQuery4)
+    pageFilter.applyToPiniaOrmQuery = vi.fn((query) => piniaOrmQuery5)
+
+    const result = query.applyToPiniaOrmQuery(piniaOrmQuery1)
+
+    expect(filter1.applyToPiniaOrmQuery).toHaveBeenCalledWith(piniaOrmQuery1)
+    expect(filter2.applyToPiniaOrmQuery).toHaveBeenCalledWith(piniaOrmQuery2)
+    expect(orderBy.applyToPiniaOrmQuery).toHaveBeenCalledWith(piniaOrmQuery3)
+    expect(pageFilter.applyToPiniaOrmQuery).toHaveBeenCalledWith(piniaOrmQuery4)
+
+    expect(result).toEqual(piniaOrmQuery5)
+  })
+})