commit
ff7b80a273
22 changed files with 691 additions and 39 deletions
25
.github/workflows/pull-request.yml
vendored
Normal file
25
.github/workflows/pull-request.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
name: Pull request workflow
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/gradle-build-action@v2
|
||||||
|
- name: Run checks with Gradle Wrapper
|
||||||
|
run: ./gradlew check
|
||||||
|
automerge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.actor == 'wbrawner' }}
|
||||||
|
steps:
|
||||||
|
- name: Enable auto-merge
|
||||||
|
run: gh pr merge --auto --merge "$PR_URL"
|
||||||
|
env:
|
||||||
|
PR_URL: ${{github.event.pull_request.html_url}}
|
||||||
|
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
@ -11,8 +11,7 @@ dependencies {
|
||||||
api(libs.ktor.server.core)
|
api(libs.ktor.server.core)
|
||||||
api(libs.ktor.serialization)
|
api(libs.ktor.serialization)
|
||||||
api(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
testImplementation(libs.junit.jupiter.api)
|
testImplementation(project(":testhelpers"))
|
||||||
testRuntimeOnly(libs.junit.jupiter.engine)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.getByName<Test>("test") {
|
tasks.getByName<Test>("test") {
|
||||||
|
|
|
@ -15,9 +15,9 @@ data class BudgetRequest(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BudgetResponse(
|
data class BudgetResponse(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String?,
|
val name: String? = null,
|
||||||
val description: String?,
|
val description: String? = null,
|
||||||
private val users: List<UserPermissionResponse>
|
val users: List<UserPermissionResponse>
|
||||||
) {
|
) {
|
||||||
constructor(budget: Budget, users: Iterable<UserPermission>) : this(
|
constructor(budget: Budget, users: Iterable<UserPermission>) : this(
|
||||||
Objects.requireNonNull<String>(budget.id),
|
Objects.requireNonNull<String>(budget.id),
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
|
@ -27,8 +26,8 @@ dependencies {
|
||||||
implementation(libs.logback)
|
implementation(libs.logback)
|
||||||
implementation(libs.mail)
|
implementation(libs.mail)
|
||||||
testImplementation(project(":testhelpers"))
|
testImplementation(project(":testhelpers"))
|
||||||
testImplementation(libs.junit.jupiter.api)
|
testImplementation(libs.ktor.client.content.negotiation)
|
||||||
testRuntimeOnly(libs.junit.jupiter.engine)
|
testImplementation(libs.ktor.server.test)
|
||||||
}
|
}
|
||||||
|
|
||||||
description = "twigs-server"
|
description = "twigs-server"
|
||||||
|
|
|
@ -12,6 +12,8 @@ import io.ktor.http.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.*
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.cio.*
|
||||||
|
import io.ktor.server.engine.*
|
||||||
import io.ktor.server.plugins.callloging.*
|
import io.ktor.server.plugins.callloging.*
|
||||||
import io.ktor.server.plugins.contentnegotiation.*
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
import io.ktor.server.plugins.cors.routing.*
|
import io.ktor.server.plugins.cors.routing.*
|
||||||
|
@ -23,10 +25,13 @@ import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.lang.RuntimeException
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
fun main(args: Array<String>): Unit = io.ktor.server.cio.EngineMain.main(args)
|
fun main() {
|
||||||
|
embeddedServer(CIO, port = System.getenv("PORT")?.toIntOrNull() ?: 8080) {
|
||||||
|
module()
|
||||||
|
}.start(wait = true)
|
||||||
|
}
|
||||||
|
|
||||||
private const val DATABASE_VERSION = 3
|
private const val DATABASE_VERSION = 3
|
||||||
|
|
||||||
|
@ -63,7 +68,7 @@ fun Application.module() {
|
||||||
username = environment.config.propertyOrNull("twigs.smtp.user")?.getString(),
|
username = environment.config.propertyOrNull("twigs.smtp.user")?.getString(),
|
||||||
password = environment.config.propertyOrNull("twigs.smtp.pass")?.getString(),
|
password = environment.config.propertyOrNull("twigs.smtp.pass")?.getString(),
|
||||||
),
|
),
|
||||||
metadataRepository = MetadataRepository(it),
|
metadataRepository = JdbcMetadataRepository(it),
|
||||||
budgetRepository = JdbcBudgetRepository(it),
|
budgetRepository = JdbcBudgetRepository(it),
|
||||||
categoryRepository = JdbcCategoryRepository(it),
|
categoryRepository = JdbcCategoryRepository(it),
|
||||||
passwordResetRepository = JdbcPasswordResetRepository(it),
|
passwordResetRepository = JdbcPasswordResetRepository(it),
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
ktor {
|
ktor {
|
||||||
deployment {
|
deployment {
|
||||||
port = 8080
|
port = 8080
|
||||||
port = ${?TWIGS_PORT}
|
port = ${?PORT}
|
||||||
}
|
|
||||||
application {
|
|
||||||
modules = [ com.wbrawner.twigs.server.ApplicationKt.module ]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ package com.wbrawner.twigs.server
|
||||||
import com.wbrawner.twigs.model.*
|
import com.wbrawner.twigs.model.*
|
||||||
import com.wbrawner.twigs.storage.RecurringTransactionRepository
|
import com.wbrawner.twigs.storage.RecurringTransactionRepository
|
||||||
import com.wbrawner.twigs.storage.TransactionRepository
|
import com.wbrawner.twigs.storage.TransactionRepository
|
||||||
import com.wbrawner.twigs.test.helpers.repository.FakeRecurringTransactionsRepository
|
import com.wbrawner.twigs.test.helpers.repository.FakeRecurringTransactionRepository
|
||||||
import com.wbrawner.twigs.test.helpers.repository.FakeTransactionRepository
|
import com.wbrawner.twigs.test.helpers.repository.FakeTransactionRepository
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
@ -21,13 +21,13 @@ class RecurringTransactionProcessingJobTest {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
recurringTransactionRepository = FakeRecurringTransactionsRepository()
|
recurringTransactionRepository = FakeRecurringTransactionRepository()
|
||||||
transactionRepository = FakeTransactionRepository()
|
transactionRepository = FakeTransactionRepository()
|
||||||
job = RecurringTransactionProcessingJob(recurringTransactionRepository, transactionRepository)
|
job = RecurringTransactionProcessingJob(recurringTransactionRepository, transactionRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `daily transactions are created every day`() = runBlockingTest {
|
fun `daily transactions are created every day`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -49,7 +49,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `daily transactions are only created once per day`() = runBlockingTest {
|
fun `daily transactions are only created once per day`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -71,7 +71,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `daily transactions are created every other day`() = runBlockingTest {
|
fun `daily transactions are created every other day`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -90,7 +90,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `weekly transactions are created every thursday`() = runBlockingTest {
|
fun `weekly transactions are created every thursday`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -113,7 +113,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `weekly transactions are created every third thursday`() = runBlockingTest {
|
fun `weekly transactions are created every third thursday`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -134,7 +134,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `monthly transactions are created every 1st of month`() = runBlockingTest {
|
fun `monthly transactions are created every 1st of month`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -157,7 +157,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `monthly transactions are created every last day of month when greater than max days in month`() =
|
fun `monthly transactions are created every last day of month when greater than max days in month`() =
|
||||||
runBlockingTest {
|
runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -180,7 +180,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `monthly transactions are created every 6 months`() = runBlockingTest {
|
fun `monthly transactions are created every 6 months`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -201,7 +201,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `monthly transactions are created every 2nd tuesday`() = runBlockingTest {
|
fun `monthly transactions are created every 2nd tuesday`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -228,7 +228,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `monthly transactions are created every last friday`() = runBlockingTest {
|
fun `monthly transactions are created every last friday`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -255,7 +255,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `monthly transactions are created in the new year`() = runBlockingTest {
|
fun `monthly transactions are created in the new year`() = runTest {
|
||||||
val start = Instant.parse("1971-01-01T00:00:00Z")
|
val start = Instant.parse("1971-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -280,7 +280,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `yearly transactions are created every march 31st`() = runBlockingTest {
|
fun `yearly transactions are created every march 31st`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -303,7 +303,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `yearly transactions are created every other march 31st`() = runBlockingTest {
|
fun `yearly transactions are created every other march 31st`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
@ -324,7 +324,7 @@ class RecurringTransactionProcessingJobTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `yearly transactions are created every february 29th`() = runBlockingTest {
|
fun `yearly transactions are created every february 29th`() = runTest {
|
||||||
val start = Instant.parse("1970-01-01T00:00:00Z")
|
val start = Instant.parse("1970-01-01T00:00:00Z")
|
||||||
recurringTransactionRepository.save(
|
recurringTransactionRepository.save(
|
||||||
RecurringTransaction(
|
RecurringTransaction(
|
||||||
|
|
60
app/src/test/kotlin/com/wbrawner/twigs/server/api/ApiTest.kt
Normal file
60
app/src/test/kotlin/com/wbrawner/twigs/server/api/ApiTest.kt
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package com.wbrawner.twigs.server.api
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.server.moduleWithDependencies
|
||||||
|
import com.wbrawner.twigs.test.helpers.FakeEmailService
|
||||||
|
import com.wbrawner.twigs.test.helpers.repository.*
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.server.testing.*
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
|
||||||
|
open class ApiTest {
|
||||||
|
lateinit var budgetRepository: FakeBudgetRepository
|
||||||
|
lateinit var categoryRepository: FakeCategoryRepository
|
||||||
|
lateinit var emailService: FakeEmailService
|
||||||
|
lateinit var metadataRepository: FakeMetadataRepository
|
||||||
|
lateinit var passwordResetRepository: FakePasswordResetRepository
|
||||||
|
lateinit var permissionRepository: FakePermissionRepository
|
||||||
|
lateinit var recurringTransactionRepository: FakeRecurringTransactionRepository
|
||||||
|
lateinit var sessionRepository: FakeSessionRepository
|
||||||
|
lateinit var transactionRepository: FakeTransactionRepository
|
||||||
|
lateinit var userRepository: FakeUserRepository
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
budgetRepository = FakeBudgetRepository()
|
||||||
|
categoryRepository = FakeCategoryRepository()
|
||||||
|
emailService = FakeEmailService()
|
||||||
|
metadataRepository = FakeMetadataRepository()
|
||||||
|
passwordResetRepository = FakePasswordResetRepository()
|
||||||
|
permissionRepository = FakePermissionRepository()
|
||||||
|
recurringTransactionRepository = FakeRecurringTransactionRepository()
|
||||||
|
sessionRepository = FakeSessionRepository()
|
||||||
|
transactionRepository = FakeTransactionRepository()
|
||||||
|
userRepository = FakeUserRepository()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun apiTest(test: suspend ApiTest.(client: HttpClient) -> Unit) = testApplication {
|
||||||
|
application {
|
||||||
|
moduleWithDependencies(
|
||||||
|
emailService = emailService,
|
||||||
|
metadataRepository = metadataRepository,
|
||||||
|
budgetRepository = budgetRepository,
|
||||||
|
categoryRepository = categoryRepository,
|
||||||
|
passwordResetRepository = passwordResetRepository,
|
||||||
|
permissionRepository = permissionRepository,
|
||||||
|
recurringTransactionRepository = recurringTransactionRepository,
|
||||||
|
sessionRepository = sessionRepository,
|
||||||
|
transactionRepository = transactionRepository,
|
||||||
|
userRepository = userRepository
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val client = createClient {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test(client)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,423 @@
|
||||||
|
package com.wbrawner.twigs.server.api
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.BudgetRequest
|
||||||
|
import com.wbrawner.twigs.BudgetResponse
|
||||||
|
import com.wbrawner.twigs.UserPermissionRequest
|
||||||
|
import com.wbrawner.twigs.UserPermissionResponse
|
||||||
|
import com.wbrawner.twigs.model.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Disabled
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class BudgetRouteTest : ApiTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fetching budgets requires authentication`() = apiTest { client ->
|
||||||
|
val response = client.get("/api/budgets")
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fetching budgets returns empty list when there are no budgets`() = apiTest { client ->
|
||||||
|
val session = Session()
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val response = client.get("/api/budgets") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
assertEquals(0, response.body<List<BudgetResponse>>().size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fetching budgets only returns budgets for current user`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val currentUserBudget = budgetRepository.save(Budget(name = "Test User's Budget"))
|
||||||
|
val otherUserBudget = budgetRepository.save(Budget(name = "Other User's Budget"))
|
||||||
|
permissionRepository.save(
|
||||||
|
UserPermission(
|
||||||
|
budgetId = currentUserBudget.id,
|
||||||
|
userId = users[0].id,
|
||||||
|
Permission.OWNER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
permissionRepository.save(UserPermission(budgetId = otherUserBudget.id, userId = users[1].id, Permission.OWNER))
|
||||||
|
val response = client.get("/api/budgets") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
}
|
||||||
|
val returnedBudgets = response.body<List<BudgetResponse>>()
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
assertEquals(1, returnedBudgets.size)
|
||||||
|
assertEquals(currentUserBudget.id, returnedBudgets.first().id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `creating budgets requires authentication`() = apiTest { client ->
|
||||||
|
val request = BudgetRequest("Test Budget", "A budget for testing")
|
||||||
|
val response = client.post("/api/budgets") {
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `newly created budgets are saved`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermissionRequest(users[0].id, Permission.OWNER),
|
||||||
|
UserPermissionRequest(users[1].id, Permission.READ),
|
||||||
|
)
|
||||||
|
val request = BudgetRequest("Test Budget", "A budget for testing", permissions)
|
||||||
|
val response = client.post("/api/budgets") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
val responseBody: BudgetResponse = response.body()
|
||||||
|
assert(responseBody.id.isNotEmpty())
|
||||||
|
assertEquals("Test Budget", responseBody.name)
|
||||||
|
assertEquals("A budget for testing", responseBody.description)
|
||||||
|
assertEquals(2, responseBody.users.size)
|
||||||
|
assert(responseBody.users.containsAll(permissions.map { UserPermissionResponse(it.user, it.permission) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `newly created budgets include current user as owner if omitted`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermissionRequest(users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
val request = BudgetRequest("Test Budget", "A budget for testing", permissions)
|
||||||
|
val response = client.post("/api/budgets") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
val responseBody: BudgetResponse = response.body()
|
||||||
|
assert(responseBody.id.isNotEmpty())
|
||||||
|
assertEquals("Test Budget", responseBody.name)
|
||||||
|
assertEquals("A budget for testing", responseBody.description)
|
||||||
|
assertEquals(2, responseBody.users.size)
|
||||||
|
val expectedPermissions = listOf(
|
||||||
|
UserPermissionResponse(users[0].id, Permission.OWNER),
|
||||||
|
UserPermissionResponse(users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
assert(responseBody.users.containsAll(expectedPermissions))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updating budgets requires authentication`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val request = BudgetRequest("Update Budget", "A budget for testing")
|
||||||
|
val response = client.put("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updating budgets returns forbidden for users with read only access`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[0].id, Permission.READ),
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
permissions.forEach {
|
||||||
|
permissionRepository.save(it)
|
||||||
|
}
|
||||||
|
val request = BudgetRequest("Update Budget", "A budget for testing")
|
||||||
|
val response = client.put("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Forbidden, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updating budgets returns forbidden for users with write only access`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[0].id, Permission.WRITE),
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
permissions.forEach {
|
||||||
|
permissionRepository.save(it)
|
||||||
|
}
|
||||||
|
val request = BudgetRequest("Update Budget", "A budget for testing")
|
||||||
|
val response = client.put("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Forbidden, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `updating budgets returns success for users with manage access`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[0].id, Permission.MANAGE),
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
permissions.forEach {
|
||||||
|
permissionRepository.save(it)
|
||||||
|
}
|
||||||
|
val request = BudgetRequest("Update Budget", "An update budget for testing")
|
||||||
|
val response = client.put("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
val updatedBudget: BudgetResponse = response.body()
|
||||||
|
assertEquals(request.name, updatedBudget.name)
|
||||||
|
assertEquals(request.description, updatedBudget.description)
|
||||||
|
val expectedUsers = permissions.map { UserPermissionResponse(it.userId, it.permission) }
|
||||||
|
val updatedUsers = updatedBudget.users
|
||||||
|
assertEquals(expectedUsers, updatedUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Disabled("Will be fixed with service layer refactor")
|
||||||
|
@Test
|
||||||
|
fun `updating budgets returns not found for users with no access`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
permissions.forEach {
|
||||||
|
permissionRepository.save(it)
|
||||||
|
}
|
||||||
|
val request = BudgetRequest("Update Budget", "An update budget for testing")
|
||||||
|
val response = client.put("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.NotFound, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Disabled("Will be fixed with service layer refactor")
|
||||||
|
@Test
|
||||||
|
fun `updating non-existent budgets returns not found`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val request = BudgetRequest("Update Budget", "An update budget for testing")
|
||||||
|
val response = client.put("/api/budgets/random-budget-id") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.NotFound, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Disabled("Will be fixed with service layer refactor")
|
||||||
|
@Test
|
||||||
|
fun `updating budgets returns forbidden for users with manage access attempting to remove owner`() =
|
||||||
|
apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget =
|
||||||
|
budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[0].id, Permission.MANAGE),
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
permissions.forEach {
|
||||||
|
permissionRepository.save(it)
|
||||||
|
}
|
||||||
|
val request = BudgetRequest(
|
||||||
|
"Update Budget",
|
||||||
|
"An update budget for testing",
|
||||||
|
setOf(
|
||||||
|
UserPermissionRequest(users[0].id, Permission.OWNER),
|
||||||
|
UserPermissionRequest(users[0].id, Permission.MANAGE),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val response = client.put("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Forbidden, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleting budgets requires authentication`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val request = BudgetRequest("Update Budget", "A budget for testing")
|
||||||
|
val response = client.put("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleting budgets returns forbidden for users with read only access`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[0].id, Permission.READ),
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
permissions.forEach {
|
||||||
|
permissionRepository.save(it)
|
||||||
|
}
|
||||||
|
val response = client.delete("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Forbidden, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleting budgets returns forbidden for users with write only access`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[0].id, Permission.WRITE),
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
permissions.forEach {
|
||||||
|
permissionRepository.save(it)
|
||||||
|
}
|
||||||
|
val response = client.delete("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Forbidden, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleting budgets returns forbidden for users with manage access`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[0].id, Permission.MANAGE),
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
permissions.forEach {
|
||||||
|
permissionRepository.save(it)
|
||||||
|
}
|
||||||
|
val response = client.delete("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Forbidden, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleting budgets returns success for users with owner access`() = apiTest { client ->
|
||||||
|
val users = listOf(
|
||||||
|
User(name = "testuser", password = "testpassword"),
|
||||||
|
User(name = "otheruser", password = "otherpassword"),
|
||||||
|
)
|
||||||
|
users.forEach { userRepository.save(it) }
|
||||||
|
val existingBudget = budgetRepository.save(Budget(name = "Test Budget", description = "A budget for testing"))
|
||||||
|
val session = Session(userId = users.first().id)
|
||||||
|
sessionRepository.save(session)
|
||||||
|
val permissions = setOf(
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[0].id, Permission.OWNER),
|
||||||
|
UserPermission(budgetId = existingBudget.id, userId = users[1].id, Permission.OWNER),
|
||||||
|
)
|
||||||
|
permissions.forEach {
|
||||||
|
permissionRepository.save(it)
|
||||||
|
}
|
||||||
|
val response = client.delete("/api/budgets/${existingBudget.id}") {
|
||||||
|
header("Authorization", "Bearer ${session.token}")
|
||||||
|
header("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.NoContent, response.status)
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,10 @@ class JdbcPermissionRepository(dataSource: DataSource) :
|
||||||
conn.executeQuery(sql.toString(), params)
|
conn.executeQuery(sql.toString(), params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun findAll(ids: List<String>?): List<UserPermission> {
|
||||||
|
throw UnsupportedOperationException("UserPermission requires a userId and budgetId")
|
||||||
|
}
|
||||||
|
|
||||||
override fun ResultSet.toEntity(): UserPermission = UserPermission(
|
override fun ResultSet.toEntity(): UserPermission = UserPermission(
|
||||||
budgetId = getString(Fields.BUDGET_ID.name.lowercase()),
|
budgetId = getString(Fields.BUDGET_ID.name.lowercase()),
|
||||||
userId = getString(Fields.USER_ID.name.lowercase()),
|
userId = getString(Fields.USER_ID.name.lowercase()),
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
package com.wbrawner.twigs.db
|
package com.wbrawner.twigs.db
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.storage.Repository
|
||||||
import java.sql.ResultSet
|
import java.sql.ResultSet
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
class MetadataRepository(dataSource: DataSource) :
|
interface MetadataRepository : Repository<DatabaseMetadata> {
|
||||||
JdbcRepository<DatabaseMetadata, MetadataRepository.Fields>(dataSource) {
|
fun runMigration(toVersion: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
class JdbcMetadataRepository(dataSource: DataSource) :
|
||||||
|
JdbcRepository<DatabaseMetadata, JdbcMetadataRepository.Fields>(dataSource), MetadataRepository {
|
||||||
override val tableName: String = TABLE_METADATA
|
override val tableName: String = TABLE_METADATA
|
||||||
override val fields: Map<Fields, (DatabaseMetadata) -> Any?> = Fields.values().associateWith { it.entityField }
|
override val fields: Map<Fields, (DatabaseMetadata) -> Any?> = Fields.values().associateWith { it.entityField }
|
||||||
override val conflictFields: Collection<String> = listOf()
|
override val conflictFields: Collection<String> = listOf()
|
||||||
|
|
||||||
suspend fun runMigration(toVersion: Int) {
|
override fun runMigration(toVersion: Int) {
|
||||||
val queries = MetadataRepository::class.java
|
val queries = MetadataRepository::class.java
|
||||||
.getResource("/sql/$toVersion.sql")
|
.getResource("/sql/$toVersion.sql")
|
||||||
?.readText()
|
?.readText()
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
bcrypt = "0.9.0"
|
bcrypt = "0.9.0"
|
||||||
hikari = "5.0.1"
|
hikari = "5.0.1"
|
||||||
junit = "5.8.2"
|
junit = "5.8.2"
|
||||||
kotlin = "1.6.21"
|
kotlin = "1.9.10"
|
||||||
kotlinx-coroutines = "1.6.2"
|
kotlinx-coroutines = "1.6.2"
|
||||||
ktor = "2.0.2"
|
ktor = "2.3.4"
|
||||||
logback = "1.2.11"
|
logback = "1.2.11"
|
||||||
mail = "1.6.2"
|
mail = "1.6.2"
|
||||||
postgres = "42.3.8"
|
postgres = "42.3.8"
|
||||||
|
@ -18,8 +18,10 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
|
||||||
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" }
|
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" }
|
||||||
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||||
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||||
|
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||||
ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||||
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
|
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
|
||||||
ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging", version.ref = "ktor" }
|
ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging", version.ref = "ktor" }
|
||||||
|
@ -28,6 +30,7 @@ ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negoti
|
||||||
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
|
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
|
||||||
ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
|
ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
|
||||||
ktor-server-sessions = { module = "io.ktor:ktor-server-sessions", version.ref = "ktor" }
|
ktor-server-sessions = { module = "io.ktor:ktor-server-sessions", version.ref = "ktor" }
|
||||||
|
ktor-server-test = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
|
||||||
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||||
mail = { module = "com.sun.mail:javax.mail", version.ref = "mail" }
|
mail = { module = "com.sun.mail:javax.mail", version.ref = "mail" }
|
||||||
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
|
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
|
||||||
|
|
|
@ -8,5 +8,12 @@ dependencies {
|
||||||
implementation(project(":storage"))
|
implementation(project(":storage"))
|
||||||
api(libs.kotlinx.coroutines.test)
|
api(libs.kotlinx.coroutines.test)
|
||||||
api(libs.junit.jupiter.api)
|
api(libs.junit.jupiter.api)
|
||||||
|
implementation(project(mapOf("path" to ":db")))
|
||||||
runtimeOnly(libs.junit.jupiter.engine)
|
runtimeOnly(libs.junit.jupiter.engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.wbrawner.twigs.test.helpers
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.EmailService
|
||||||
|
import com.wbrawner.twigs.model.PasswordResetToken
|
||||||
|
|
||||||
|
class FakeEmailService : EmailService {
|
||||||
|
val emails = mutableListOf<FakeEmail<*>>()
|
||||||
|
|
||||||
|
override fun sendPasswordResetEmail(token: PasswordResetToken, to: String) {
|
||||||
|
emails.add(FakeEmail(to, token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FakeEmail<Data>(val to: String, val data: Data)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.wbrawner.twigs.test.helpers.repository
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.model.Budget
|
||||||
|
import com.wbrawner.twigs.storage.BudgetRepository
|
||||||
|
|
||||||
|
class FakeBudgetRepository : FakeRepository<Budget>(), BudgetRepository
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.wbrawner.twigs.test.helpers.repository
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.model.Category
|
||||||
|
import com.wbrawner.twigs.storage.CategoryRepository
|
||||||
|
|
||||||
|
class FakeCategoryRepository : FakeRepository<Category>(), CategoryRepository {
|
||||||
|
override fun findAll(
|
||||||
|
budgetIds: List<String>,
|
||||||
|
ids: List<String>?,
|
||||||
|
expense: Boolean?,
|
||||||
|
archived: Boolean?
|
||||||
|
): List<Category> = entities.filter {
|
||||||
|
budgetIds.contains(it.budgetId)
|
||||||
|
&& ids?.contains(it.id) ?: true
|
||||||
|
&& it.expense == (expense ?: it.expense)
|
||||||
|
&& it.archived == (archived ?: it.archived)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.wbrawner.twigs.test.helpers.repository
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.db.DatabaseMetadata
|
||||||
|
import com.wbrawner.twigs.db.MetadataRepository
|
||||||
|
import com.wbrawner.twigs.storage.Repository
|
||||||
|
|
||||||
|
class FakeMetadataRepository : Repository<DatabaseMetadata>, MetadataRepository {
|
||||||
|
var metadata = DatabaseMetadata()
|
||||||
|
override fun runMigration(toVersion: Int) {
|
||||||
|
metadata = metadata.copy(version = toVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findAll(ids: List<String>?): List<DatabaseMetadata> = listOf(metadata)
|
||||||
|
|
||||||
|
override suspend fun delete(item: DatabaseMetadata): Boolean = false
|
||||||
|
|
||||||
|
override suspend fun save(item: DatabaseMetadata): DatabaseMetadata {
|
||||||
|
metadata = item
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.wbrawner.twigs.test.helpers.repository
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.model.PasswordResetToken
|
||||||
|
import com.wbrawner.twigs.storage.PasswordResetRepository
|
||||||
|
|
||||||
|
class FakePasswordResetRepository : FakeRepository<PasswordResetToken>(), PasswordResetRepository
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.wbrawner.twigs.test.helpers.repository
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.model.UserPermission
|
||||||
|
import com.wbrawner.twigs.storage.PermissionRepository
|
||||||
|
|
||||||
|
class FakePermissionRepository : PermissionRepository {
|
||||||
|
val permissions: MutableList<UserPermission> = mutableListOf()
|
||||||
|
override fun findAll(budgetIds: List<String>?, userId: String?): List<UserPermission> =
|
||||||
|
permissions.filter { userPermission ->
|
||||||
|
budgetIds?.contains(userPermission.budgetId) ?: true
|
||||||
|
&& userId?.let { it == userPermission.userId } ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findAll(ids: List<String>?): List<UserPermission> {
|
||||||
|
throw UnsupportedOperationException("UserPermission requires a userId and budgetId")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(item: UserPermission): UserPermission {
|
||||||
|
permissions.removeIf { it.budgetId == item.budgetId && it.userId == item.userId }
|
||||||
|
permissions.add(item)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(item: UserPermission): Boolean =
|
||||||
|
permissions.removeIf { it.budgetId == item.budgetId && it.userId == item.userId }
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import com.wbrawner.twigs.model.RecurringTransaction
|
||||||
import com.wbrawner.twigs.storage.RecurringTransactionRepository
|
import com.wbrawner.twigs.storage.RecurringTransactionRepository
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
class FakeRecurringTransactionsRepository : FakeRepository<RecurringTransaction>(), RecurringTransactionRepository {
|
class FakeRecurringTransactionRepository : FakeRepository<RecurringTransaction>(), RecurringTransactionRepository {
|
||||||
override suspend fun findAll(now: Instant): List<RecurringTransaction> = entities.filter {
|
override suspend fun findAll(now: Instant): List<RecurringTransaction> = entities.filter {
|
||||||
(it.start == now || it.start.isBefore(now)) && it.finish?.isAfter(now) ?: true
|
(it.start == now || it.start.isBefore(now)) && it.finish?.isAfter(now) ?: true
|
||||||
}
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.wbrawner.twigs.test.helpers.repository
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.model.Session
|
||||||
|
import com.wbrawner.twigs.storage.SessionRepository
|
||||||
|
import java.util.function.Predicate
|
||||||
|
|
||||||
|
class FakeSessionRepository : FakeRepository<Session>(), SessionRepository {
|
||||||
|
var expirationPredicate: Predicate<in Session> = Predicate { false }
|
||||||
|
|
||||||
|
override fun findAll(token: String): List<Session> = entities.filter { it.token == token }
|
||||||
|
|
||||||
|
override fun deleteExpired() {
|
||||||
|
entities.removeIf(expirationPredicate)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.wbrawner.twigs.test.helpers.repository
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.model.User
|
||||||
|
import com.wbrawner.twigs.storage.UserRepository
|
||||||
|
|
||||||
|
class FakeUserRepository : FakeRepository<User>(), UserRepository {
|
||||||
|
override fun findAll(nameOrEmail: String, password: String?): List<User> {
|
||||||
|
return entities.filter {
|
||||||
|
(it.name.equals(nameOrEmail, ignoreCase = true) || it.email.equals(
|
||||||
|
nameOrEmail,
|
||||||
|
ignoreCase = true
|
||||||
|
)) && it.password == password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findAll(nameLike: String): List<User> {
|
||||||
|
return entities.filter { it.name.contains(nameLike, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue