Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release #199

Merged
merged 8 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ plugins {
id("org.unbroken-dome.test-sets") version "4.0.0"
}

testSets {
register("migrationTest")
}

dependencies {
implementation(project(":core"))

Expand All @@ -15,6 +11,9 @@ dependencies {
implementation("io.jsonwebtoken:jjwt:0.9.1")
runtimeOnly("javax.xml.bind:jaxb-api:2.1")

testImplementation(testFixtures(project(":core")))
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:mongodb:1.19.0")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.2")
}

Expand Down
10 changes: 0 additions & 10 deletions api/src/migrationTest/kotlin/Migration.kt

This file was deleted.

57 changes: 0 additions & 57 deletions api/src/migrationTest/kotlin/MigrationConfig.kt

This file was deleted.

33 changes: 0 additions & 33 deletions api/src/migrationTest/kotlin/handler/TimetableMigrations.kt

This file was deleted.

4 changes: 0 additions & 4 deletions api/src/migrationTest/resources/application-migration.yaml

This file was deleted.

12 changes: 12 additions & 0 deletions api/src/test/kotlin/BaseIntegTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import io.kotest.core.spec.style.WordSpec
import mock.MockMongoDB
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import

@SpringBootTest
@Import(MockMongoDB::class)
abstract class BaseIntegTest(body: BaseIntegTest.() -> Unit = {}) : WordSpec() {
init {
body()
}
}
111 changes: 111 additions & 0 deletions api/src/test/kotlin/timetable/TimetableIntegTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.wafflestudio.snu4t.timetable

import BaseIntegTest
import com.ninjasquad.springmockk.MockkBean
import com.wafflestudio.snu4t.fixture.TimetableFixture
import com.wafflestudio.snu4t.fixture.UserFixture
import com.wafflestudio.snu4t.handler.RequestContext
import com.wafflestudio.snu4t.middleware.SnuttRestApiDefaultMiddleware
import com.wafflestudio.snu4t.router.MainRouter
import com.wafflestudio.snu4t.timetables.dto.TimetableLegacyDto
import com.wafflestudio.snu4t.timetables.repository.TimetableRepository
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.mockk.coEvery
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
import org.springframework.http.MediaType
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
import timetables.dto.TimetableBriefDto

@SpringBootTest
class TimetableIntegTest(
@MockkBean private val mockMiddleware: SnuttRestApiDefaultMiddleware,
val mainRouter: MainRouter,
val timetableFixture: TimetableFixture,
val userFixture: UserFixture,
val timetableRepository: TimetableRepository,
val repositories: List<CoroutineCrudRepository<*, *>>
) : BaseIntegTest({
val timetableServer =
WebTestClient.bindToRouterFunction(mainRouter.tableRoute()).configureClient().defaultHeaders { header ->
header.contentType = MediaType.APPLICATION_JSON
}.build()

coEvery { mockMiddleware.invoke(any(), any()) } returns RequestContext(user = userFixture.testUser)
afterContainer { repositories.forEach { it.deleteAll() } }

"POST /v1/tables" should {
"success" {
timetableServer.post().uri("/v1/tables")
.bodyValue("""{"year":2016, "semester":3, "title":"MyTimeTable"}""".trimIndent()).exchange()
.expectStatus().isOk.expectBody<List<TimetableBriefDto>>().returnResult().responseBody.should { body ->
body.shouldNotBeNull()
body.size shouldBe 1
body[0].title shouldBe "MyTimeTable"
body[0].year shouldBe 2016
body[0].semester shouldBe 3
}
}
}

"GET /v1/tables 요청시" should {
val table = timetableFixture.getTimetable("test").let { timetableRepository.save(it) }
"정상 반환" {
timetableServer.get().uri("/v1/tables").exchange()
.expectStatus().isOk.expectBody<List<TimetableBriefDto>>()
.returnResult().responseBody.should { body ->
body.shouldNotBeNull()
body.size shouldBe 1
body[0].id shouldBe table.id
body[0].title shouldBe table.title
body[0].year shouldBe table.year
body[0].semester shouldBe table.semester.value
body[0].isPrimary shouldBe false
body[0].totalCredit shouldBe 0
}
}
"json 형태 확인" {
timetableServer.get().uri("/v1/tables").exchange().expectStatus().isOk.expectBody()
.jsonPath("$.[0]._id").exists()
.jsonPath("$.[0].title").exists()
.jsonPath("$.[0].year").isNumber
.jsonPath("$.[0].semester").isNumber
.jsonPath("$.[0].isPrimary").isBoolean
.jsonPath("$.[0].updated_at").exists()
.jsonPath("$.[0].total_credit").exists()
}
}
"GET /v1/tables/{tableId} 요청 시" should {
val table = timetableFixture.getTimetable("test").let { timetableRepository.save(it) }
"정상 반환" {
timetableServer.get().uri("/v1/tables/${table.id}").exchange()
.expectStatus().isOk.expectBody<TimetableLegacyDto>()
.returnResult().responseBody.should { body ->
body.shouldNotBeNull()
body.id shouldBe table.id
body.title shouldBe table.title
body.year shouldBe table.year
body.semester shouldBe table.semester
body.isPrimary shouldBe false
body.theme shouldBe table.theme
body.userId shouldBe table.userId
body.lectures shouldBe emptyList()
}
}
"json 형태 확인" {
timetableServer.get().uri("/v1/tables/${table.id}").exchange().expectStatus().isOk.expectBody()
.jsonPath("$._id").exists()
.jsonPath("$.user_id").exists()
.jsonPath("$.year").isNumber
.jsonPath("$.semester").isNumber
.jsonPath("$.lecture_list").isArray
.jsonPath("$.title").exists()
.jsonPath("$.theme").isNumber
.jsonPath("$.isPrimary").isBoolean
.jsonPath("$.updated_at").exists()
}
}
})
3 changes: 3 additions & 0 deletions api/src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
spring:
profiles:
active: test
2 changes: 1 addition & 1 deletion batch/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies {
implementation("org.jsoup:jsoup:1.16.1")

// excel
implementation("org.apache.poi:poi-ooxml:5.2.3")
implementation("org.apache.poi:poi-ooxml:5.2.5")

testImplementation("org.springframework.batch:spring-batch-test")
}
Expand Down
30 changes: 24 additions & 6 deletions batch/src/main/kotlin/sugangsnu/common/SugangSnuRepository.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.wafflestudio.snu4t.sugangsnu.common

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.wafflestudio.snu4t.common.enum.Semester
import com.wafflestudio.snu4t.sugangsnu.common.api.SugangSnuApi
import com.wafflestudio.snu4t.sugangsnu.common.data.SugangSnuCoursebookCondition
import com.wafflestudio.snu4t.sugangsnu.common.enum.LectureCategory
import com.wafflestudio.snu4t.sugangsnu.common.data.SugangSnuLectureInfo
import com.wafflestudio.snu4t.sugangsnu.common.utils.toSugangSnuSearchString
import org.springframework.core.io.buffer.PooledDataBuffer
import org.springframework.http.MediaType
Expand All @@ -15,12 +17,15 @@ import org.springframework.web.reactive.function.client.createExceptionAndAwait
@Component
class SugangSnuRepository(
private val sugangSnuApi: SugangSnuApi,
private val objectMapper: ObjectMapper,
) {
companion object {
const val SUGANG_SNU_COURSEBOOK_PATH = "/sugang/cc/cc100ajax.action"
const val DEFAULT_COURSEBOOK_PARAMS = "openUpDeptCd=&openDeptCd="
const val SUGANG_SNU_SEARCH_PATH = "/sugang/cc/cc100InterfaceSrch.action"
const val DEFAULT_SEARCH_PAGE_PARAMS = "workType=S&sortKey=&sortOrder="
const val SUGANG_SNU_SEARCH_POPUP_PATH = "/sugang/cc/cc101ajax.action"
const val DEFAULT_SEARCH_POPUP_PARAMS = """t_profPersNo=&workType=+&sbjtSubhCd=000"""
const val SUGANG_SNU_LECTURE_EXCEL_DOWNLOAD_PATH = "/sugang/cc/cc100InterfaceExcel.action"
const val DEFAULT_LECTURE_EXCEL_DOWNLOAD_PARAMS =
"""seeMore=더보기&srchBdNo=&srchCamp=&srchOpenSbjtFldCd=&srchCptnCorsFg=&srchCurrPage=1&srchExcept=&srchGenrlRemoteLtYn=&srchIsEngSbjt=&srchIsPendingCourse=&srchLsnProgType=&srchMrksApprMthdChgPosbYn=&srchMrksGvMthd=&srchOpenUpDeptCd=&srchOpenMjCd=&srchOpenPntMax=&srchOpenPntMin=&srchOpenSbjtDayNm=&srchOpenSbjtNm=&srchOpenSbjtTm=&srchOpenSbjtTmNm=&srchOpenShyr=&srchOpenSubmattCorsFg=&srchOpenSubmattFgCd1=&srchOpenSubmattFgCd2=&srchOpenSubmattFgCd3=&srchOpenSubmattFgCd4=&srchOpenSubmattFgCd5=&srchOpenSubmattFgCd6=&srchOpenSubmattFgCd7=&srchOpenSubmattFgCd8=&srchOpenSubmattFgCd9=&srchOpenDeptCd=&srchOpenUpSbjtFldCd=&srchPageSize=9999&srchProfNm=&srchSbjtCd=&srchSbjtNm=&srchTlsnAplyCapaCntMax=&srchTlsnAplyCapaCntMin=&srchTlsnRcntMax=&srchTlsnRcntMin=&workType=EX"""
Expand All @@ -37,6 +42,24 @@ class SugangSnuRepository(
.retrieve()
.awaitBody<PooledDataBuffer>()

suspend fun getLectureInfo(
year: Int,
semester: Semester,
courseNumber: String,
lectureNumber: String
): SugangSnuLectureInfo = sugangSnuApi.get().uri { builder ->
val semesterSearchString = semester.toSugangSnuSearchString()
builder.path(SUGANG_SNU_SEARCH_POPUP_PATH)
.query(DEFAULT_SEARCH_POPUP_PARAMS)
.queryParam("openSchyy", year)
.queryParam("openShtmFg", semesterSearchString.substring(0..9))
.queryParam("openDetaShtmFg", semesterSearchString.substring(10))
.queryParam("sbjtCd", courseNumber)
.queryParam("ltNo", lectureNumber)
.build()
}.accept(MediaType.APPLICATION_JSON).retrieve().awaitBody<String>()
.let { objectMapper.readValue<SugangSnuLectureInfo>(it) }

suspend fun getCoursebookCondition(): SugangSnuCoursebookCondition =
sugangSnuApi.get().uri { builder ->
builder.path(SUGANG_SNU_COURSEBOOK_PATH)
Expand All @@ -53,7 +76,6 @@ class SugangSnuRepository(
suspend fun getSugangSnuLectures(
year: Int,
semester: Semester,
lectureCategory: LectureCategory = LectureCategory.NONE,
language: String = "ko",
): PooledDataBuffer =
sugangSnuApi.get().uri { builder ->
Expand All @@ -63,10 +85,6 @@ class SugangSnuRepository(
queryParam("srchLanguage", language)
queryParam("srchOpenSchyy", year)
queryParam("srchOpenShtm", semester.toSugangSnuSearchString())
if (lectureCategory != LectureCategory.NONE) {
replaceQueryParam("srchOpenSbjtFldCd", lectureCategory.queryValue)
replaceQueryParam("srchOpenUpSbjtFldCd", lectureCategory.parentCategory)
}
build()
}
}.accept(MediaType.TEXT_HTML).awaitExchange {
Expand Down
Loading
Loading