Skip to content

Commit

Permalink
feat(messaging): 알림 발송을 위해 기기를 등록한다 (#183)
Browse files Browse the repository at this point in the history
* feat(messaging): 알림 발송을 위해 기기를 등록한다

* feat(messaging): 한 기기는 한 계정당 한 번만 등록할 수 있다

* feat(messaging.notification): endpoint 수정

* feat(messaging.notification): endpoint 수정
  • Loading branch information
0chil authored Nov 3, 2024
1 parent a5de0fe commit 63c465a
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
public enum Kind {

INTERNAL_SERVER_ERROR(true, HttpStatus.INTERNAL_SERVER_ERROR),
BAD_REQUEST(false, HttpStatus.BAD_REQUEST);
BAD_REQUEST(false, HttpStatus.BAD_REQUEST),
IGNORE(false, HttpStatus.OK);

private final boolean needsMessageToBeHidden;
private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.snackgame.server.messaging.notification.controller

import com.snackgame.server.auth.token.support.Authenticated
import com.snackgame.server.member.domain.Member
import com.snackgame.server.messaging.notification.service.NotificationService
import com.snackgame.server.messaging.notification.service.dto.DeviceTokenRequest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@Tag(name = "🔔 알림")
@RestController
class NotificationController(private val notificationService: NotificationService) {

@Operation(summary = "기기 등록", description = "기기를 등록한다")
@PostMapping("/notifications/devices")
fun registerDevice(
@Authenticated member: Member,
@RequestBody deviceTokenRequest: DeviceTokenRequest
) {
notificationService.registerDeviceFor(member.id, deviceTokenRequest)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.snackgame.server.messaging.notification.domain

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Table
import javax.persistence.UniqueConstraint

@Entity
@Table(uniqueConstraints = [UniqueConstraint(columnNames = ["ownerId", "token"])])
class Device(
val ownerId: Long,
val token: String,
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.snackgame.server.messaging.notification.domain

import org.springframework.data.jpa.repository.JpaRepository

interface DeviceRepository : JpaRepository<Device, Long> {

fun existsByOwnerIdAndToken(ownerId: Long, token: String): Boolean

fun findAllByOwnerId(ownerId: Long): List<Device>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.snackgame.server.messaging.notification.exception

import com.snackgame.server.common.exception.Kind

class DuplicatedDeviceException : NotificationException("이미 등록된 기기입니다", Kind.IGNORE)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.snackgame.server.messaging.notification.exception

import com.snackgame.server.common.exception.BusinessException
import com.snackgame.server.common.exception.Kind

abstract class NotificationException(
message: String,
kind: Kind = Kind.BAD_REQUEST
) : BusinessException(kind, message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.snackgame.server.messaging.notification.service

import com.snackgame.server.messaging.notification.domain.Device
import com.snackgame.server.messaging.notification.domain.DeviceRepository
import com.snackgame.server.messaging.notification.exception.DuplicatedDeviceException
import com.snackgame.server.messaging.notification.service.dto.DeviceResponse
import com.snackgame.server.messaging.notification.service.dto.DeviceTokenRequest
import org.springframework.stereotype.Service

@Service
class NotificationService(private val deviceRepository: DeviceRepository) {

fun registerDeviceFor(ownerId: Long, deviceToken: DeviceTokenRequest) {
if (!deviceRepository.existsByOwnerIdAndToken(ownerId, deviceToken.token)) {
Device(ownerId, deviceToken.token)
.let { deviceRepository.save(it) }
return
}
throw DuplicatedDeviceException()
}

fun getDevicesOf(ownerId: Long): List<DeviceResponse> {
return deviceRepository.findAllByOwnerId(ownerId)
.map { DeviceResponse.of(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.snackgame.server.messaging.notification.service.dto

import com.snackgame.server.messaging.notification.domain.Device

data class DeviceResponse(
val ownerId: Long,
val token: String,
val id: Long
) {

companion object {

fun of(device: Device) = DeviceResponse(device.ownerId, device.token, device.id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.snackgame.server.messaging.notification.service.dto

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.Nulls

data class DeviceTokenRequest @JsonCreator constructor(
@JsonSetter(nulls = Nulls.FAIL)
val token: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@file:Suppress("NonAsciiCharacters")

package com.snackgame.server.messaging.notification.controller

import com.snackgame.server.member.fixture.MemberFixture
import com.snackgame.server.member.fixture.MemberFixture.땡칠_인증정보
import com.snackgame.server.messaging.notification.service.dto.DeviceTokenRequest
import com.snackgame.server.support.restassured.RestAssuredTest
import com.snackgame.server.support.restassured.RestAssuredUtil
import io.restassured.http.ContentType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.http.HttpStatus

@RestAssuredTest
class NotificationControllerTest {

@BeforeEach
fun setUp() {
MemberFixture.saveAll()
}

@Test
fun `기기를 등록한다`() {
RestAssuredUtil.givenAuthentication(땡칠_인증정보())
.contentType(ContentType.JSON)
.body(DeviceTokenRequest("a_device_token"))
.`when`().post("/notifications/devices")
.then().statusCode(HttpStatus.OK.value())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@file:Suppress("NonAsciiCharacters")

package com.snackgame.server.messaging.notification.service

import com.snackgame.server.member.fixture.MemberFixture.땡칠
import com.snackgame.server.messaging.notification.exception.DuplicatedDeviceException
import com.snackgame.server.messaging.notification.service.dto.DeviceTokenRequest
import com.snackgame.server.support.general.ServiceTest
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired

@ServiceTest
class NotificationServiceTest {

@Autowired
private lateinit var notificationService: NotificationService

@Test
fun `기기를 등록한다`() {
val deviceToken = "a_device_token"
notificationService.registerDeviceFor(땡칠().id, DeviceTokenRequest(deviceToken))

assertThat(notificationService.getDevicesOf(땡칠().id))
.singleElement()
.matches { it.ownerId == 땡칠().id }
.matches { it.token == deviceToken }
}

@Test
fun `한 기기는 한 계정당 한 번만 등록할 수 있다`() {
val deviceToken = "a_device_token"
notificationService.registerDeviceFor(땡칠().id, DeviceTokenRequest(deviceToken))

assertThatThrownBy { notificationService.registerDeviceFor(땡칠().id, DeviceTokenRequest(deviceToken)) }
.isInstanceOf(DuplicatedDeviceException::class.java)
}
}

0 comments on commit 63c465a

Please sign in to comment.