mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
feat(api): add DELETE /v2/characters/{id} route, v2 test coverage, and OpenAPI spec
Add REST-idiomatic DELETE method as alias for POST .../delete. Add 8 router-level tests exercising Bearer auth, invalid tokens, soft delete, export body decoding, and MaxLauncherHR capping. Create OpenAPI 3.1.0 specification covering all v2 endpoints.
This commit is contained in:
576
docs/openapi.yaml
Normal file
576
docs/openapi.yaml
Normal file
@@ -0,0 +1,576 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Erupe API
|
||||
description: REST API for the Erupe Monster Hunter Frontier server emulator.
|
||||
version: 2.0.0
|
||||
license:
|
||||
name: MIT
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8080
|
||||
description: Local development server
|
||||
|
||||
paths:
|
||||
/v2/login:
|
||||
post:
|
||||
summary: Authenticate user
|
||||
operationId: login
|
||||
tags: [auth]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LoginRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Successful authentication
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AuthData"
|
||||
"400":
|
||||
description: Invalid credentials or malformed request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
examples:
|
||||
invalid_username:
|
||||
value:
|
||||
error: invalid_username
|
||||
message: Username not found
|
||||
invalid_password:
|
||||
value:
|
||||
error: invalid_password
|
||||
message: Incorrect password
|
||||
invalid_request:
|
||||
value:
|
||||
error: invalid_request
|
||||
message: Malformed request body
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/v2/register:
|
||||
post:
|
||||
summary: Create new user account
|
||||
operationId: register
|
||||
tags: [auth]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/RegisterRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Account created successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AuthData"
|
||||
"400":
|
||||
description: Validation error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
examples:
|
||||
missing_fields:
|
||||
value:
|
||||
error: missing_fields
|
||||
message: Username and password required
|
||||
username_exists:
|
||||
value:
|
||||
error: username_exists
|
||||
message: Username already taken
|
||||
invalid_request:
|
||||
value:
|
||||
error: invalid_request
|
||||
message: Malformed request body
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/v2/launcher:
|
||||
get:
|
||||
summary: Get launcher UI data
|
||||
operationId: getLauncher
|
||||
tags: [public]
|
||||
responses:
|
||||
"200":
|
||||
description: Launcher banners, messages, and links
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LauncherResponse"
|
||||
|
||||
/v2/version:
|
||||
get:
|
||||
summary: Get server version
|
||||
operationId: getVersion
|
||||
tags: [public]
|
||||
responses:
|
||||
"200":
|
||||
description: Server version information
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/VersionResponse"
|
||||
|
||||
/v2/health:
|
||||
get:
|
||||
summary: Health check
|
||||
operationId: getHealth
|
||||
tags: [public]
|
||||
responses:
|
||||
"200":
|
||||
description: Server is healthy
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthResponse"
|
||||
"503":
|
||||
description: Server is unhealthy
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthResponse"
|
||||
|
||||
/v2/server/status:
|
||||
get:
|
||||
summary: Get server status
|
||||
operationId: getServerStatus
|
||||
tags: [public]
|
||||
responses:
|
||||
"200":
|
||||
description: Current server status including events and MezFes schedule
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ServerStatusResponse"
|
||||
|
||||
/v2/characters:
|
||||
post:
|
||||
summary: Create a new character
|
||||
operationId: createCharacter
|
||||
tags: [characters]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Character created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Character"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/v2/characters/{id}/delete:
|
||||
post:
|
||||
summary: Delete a character (POST form)
|
||||
operationId: deleteCharacterPost
|
||||
tags: [characters]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/characterId"
|
||||
responses:
|
||||
"200":
|
||||
description: Character deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"400":
|
||||
description: Invalid character ID
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/v2/characters/{id}:
|
||||
delete:
|
||||
summary: Delete a character
|
||||
operationId: deleteCharacter
|
||||
tags: [characters]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/characterId"
|
||||
responses:
|
||||
"200":
|
||||
description: Character deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"400":
|
||||
description: Invalid character ID
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
/v2/characters/{id}/export:
|
||||
get:
|
||||
summary: Export character save data
|
||||
operationId: exportSave
|
||||
tags: [characters]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/characterId"
|
||||
responses:
|
||||
"200":
|
||||
description: Full character data for backup
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ExportData"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"400":
|
||||
description: Invalid character ID
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: Session token returned by /v2/login or /v2/register
|
||||
|
||||
parameters:
|
||||
characterId:
|
||||
name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: uint32
|
||||
description: Character ID
|
||||
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid Bearer token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
example:
|
||||
error: unauthorized
|
||||
message: Invalid or expired token
|
||||
InternalError:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
example:
|
||||
error: internal_error
|
||||
message: Internal server error
|
||||
|
||||
schemas:
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required: [error, message]
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Machine-readable error code
|
||||
examples:
|
||||
- invalid_username
|
||||
- invalid_password
|
||||
- username_exists
|
||||
- missing_fields
|
||||
- invalid_request
|
||||
- unauthorized
|
||||
- internal_error
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable error description
|
||||
|
||||
LoginRequest:
|
||||
type: object
|
||||
required: [username, password]
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required: [username, password]
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
|
||||
AuthData:
|
||||
type: object
|
||||
required:
|
||||
- currentTs
|
||||
- expiryTs
|
||||
- entranceCount
|
||||
- notices
|
||||
- user
|
||||
- characters
|
||||
- courses
|
||||
- mezFes
|
||||
- patchServer
|
||||
properties:
|
||||
currentTs:
|
||||
type: integer
|
||||
format: uint32
|
||||
description: Current server timestamp (Unix seconds)
|
||||
expiryTs:
|
||||
type: integer
|
||||
format: uint32
|
||||
description: Return expiry timestamp (Unix seconds)
|
||||
entranceCount:
|
||||
type: integer
|
||||
format: uint32
|
||||
notices:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
user:
|
||||
$ref: "#/components/schemas/User"
|
||||
characters:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Character"
|
||||
courses:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/CourseInfo"
|
||||
mezFes:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/MezFes"
|
||||
- type: "null"
|
||||
patchServer:
|
||||
type: string
|
||||
|
||||
User:
|
||||
type: object
|
||||
required: [tokenId, token, rights]
|
||||
properties:
|
||||
tokenId:
|
||||
type: integer
|
||||
format: uint32
|
||||
token:
|
||||
type: string
|
||||
rights:
|
||||
type: integer
|
||||
format: uint32
|
||||
|
||||
Character:
|
||||
type: object
|
||||
required: [id, name, isFemale, weapon, hr, gr, lastLogin, returning]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: uint32
|
||||
name:
|
||||
type: string
|
||||
isFemale:
|
||||
type: boolean
|
||||
weapon:
|
||||
type: integer
|
||||
format: uint32
|
||||
hr:
|
||||
type: integer
|
||||
format: uint32
|
||||
gr:
|
||||
type: integer
|
||||
format: uint32
|
||||
lastLogin:
|
||||
type: integer
|
||||
format: int32
|
||||
description: Unix timestamp of last login
|
||||
returning:
|
||||
type: boolean
|
||||
description: True if character has not logged in for 90+ days
|
||||
|
||||
CourseInfo:
|
||||
type: object
|
||||
required: [id, name]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: uint16
|
||||
name:
|
||||
type: string
|
||||
|
||||
MezFes:
|
||||
type: object
|
||||
required: [id, start, end, soloTickets, groupTickets, stalls]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: uint32
|
||||
start:
|
||||
type: integer
|
||||
format: uint32
|
||||
end:
|
||||
type: integer
|
||||
format: uint32
|
||||
soloTickets:
|
||||
type: integer
|
||||
format: uint32
|
||||
groupTickets:
|
||||
type: integer
|
||||
format: uint32
|
||||
stalls:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
format: uint32
|
||||
|
||||
LauncherResponse:
|
||||
type: object
|
||||
required: [banners, messages, links]
|
||||
properties:
|
||||
banners:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Banner"
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Message"
|
||||
links:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Link"
|
||||
|
||||
Banner:
|
||||
type: object
|
||||
required: [src, link]
|
||||
properties:
|
||||
src:
|
||||
type: string
|
||||
description: Displayed image URL
|
||||
link:
|
||||
type: string
|
||||
description: Link accessed on click
|
||||
|
||||
Message:
|
||||
type: object
|
||||
required: [message, date, kind, link]
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: Displayed message
|
||||
date:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Displayed date (Unix timestamp)
|
||||
kind:
|
||||
type: integer
|
||||
description: "0 = Default, 1 = New"
|
||||
enum: [0, 1]
|
||||
link:
|
||||
type: string
|
||||
description: Link accessed on click
|
||||
|
||||
Link:
|
||||
type: object
|
||||
required: [name, icon, link]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Displayed name
|
||||
icon:
|
||||
type: string
|
||||
description: Displayed icon (rendered as monochrome if transparent)
|
||||
link:
|
||||
type: string
|
||||
description: Link accessed on click
|
||||
|
||||
VersionResponse:
|
||||
type: object
|
||||
required: [clientMode, name]
|
||||
properties:
|
||||
clientMode:
|
||||
type: string
|
||||
description: Supported game client version (e.g. "ZZ")
|
||||
name:
|
||||
type: string
|
||||
description: Server software name
|
||||
examples: ["Erupe-CE"]
|
||||
|
||||
HealthResponse:
|
||||
type: object
|
||||
required: [status]
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [ok, unhealthy]
|
||||
error:
|
||||
type: string
|
||||
description: Error description (present only when unhealthy)
|
||||
|
||||
ServerStatusResponse:
|
||||
type: object
|
||||
required: [mezFes, featuredWeapon, events]
|
||||
properties:
|
||||
mezFes:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/MezFes"
|
||||
- type: "null"
|
||||
featuredWeapon:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/FeatureWeaponInfo"
|
||||
- type: "null"
|
||||
events:
|
||||
$ref: "#/components/schemas/EventStatus"
|
||||
|
||||
FeatureWeaponInfo:
|
||||
type: object
|
||||
required: [startTime, activeFeatures]
|
||||
properties:
|
||||
startTime:
|
||||
type: integer
|
||||
format: uint32
|
||||
description: Unix timestamp
|
||||
activeFeatures:
|
||||
type: integer
|
||||
format: uint32
|
||||
description: Bitmask of active featured weapons
|
||||
|
||||
EventStatus:
|
||||
type: object
|
||||
required: [festaActive, divaActive]
|
||||
properties:
|
||||
festaActive:
|
||||
type: boolean
|
||||
divaActive:
|
||||
type: boolean
|
||||
|
||||
ExportData:
|
||||
type: object
|
||||
required: [character]
|
||||
properties:
|
||||
character:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Full character database row as key-value pairs
|
||||
@@ -85,6 +85,7 @@ func (s *APIServer) Start() error {
|
||||
v2Auth.Use(s.AuthMiddleware)
|
||||
v2Auth.HandleFunc("/characters", s.CreateCharacter).Methods("POST")
|
||||
v2Auth.HandleFunc("/characters/{id}/delete", s.DeleteCharacter).Methods("POST")
|
||||
v2Auth.HandleFunc("/characters/{id}", s.DeleteCharacter).Methods("DELETE")
|
||||
v2Auth.HandleFunc("/characters/{id}/export", s.ExportSave).Methods("GET")
|
||||
|
||||
handler := handlers.CORS(
|
||||
|
||||
@@ -40,6 +40,7 @@ func newTestRouter(s *APIServer) *mux.Router {
|
||||
v2Auth.Use(s.AuthMiddleware)
|
||||
v2Auth.HandleFunc("/characters", s.CreateCharacter).Methods("POST")
|
||||
v2Auth.HandleFunc("/characters/{id}/delete", s.DeleteCharacter).Methods("POST")
|
||||
v2Auth.HandleFunc("/characters/{id}", s.DeleteCharacter).Methods("DELETE")
|
||||
v2Auth.HandleFunc("/characters/{id}/export", s.ExportSave).Methods("GET")
|
||||
|
||||
v2.HandleFunc("/server/status", s.ServerStatus).Methods("GET")
|
||||
@@ -298,6 +299,201 @@ func TestV2ServerStatusRoute_WithEvents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2CreateCharacter_InvalidToken(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
sessionRepo: &mockAPISessionRepo{
|
||||
userIDErr: sql.ErrNoRows,
|
||||
},
|
||||
}
|
||||
|
||||
router := newTestRouter(server)
|
||||
|
||||
req := httptest.NewRequest("POST", "/v2/characters", nil)
|
||||
req.Header.Set("Authorization", "Bearer bad-token")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("POST /v2/characters (bad token): status = %d, want 401", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2DeleteCharacter_InvalidToken(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
sessionRepo: &mockAPISessionRepo{
|
||||
userIDErr: sql.ErrNoRows,
|
||||
},
|
||||
}
|
||||
|
||||
router := newTestRouter(server)
|
||||
|
||||
req := httptest.NewRequest("POST", "/v2/characters/5/delete", nil)
|
||||
req.Header.Set("Authorization", "Bearer bad-token")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("POST /v2/characters/5/delete (bad token): status = %d, want 401", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2DeleteCharacter_DELETE(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
sessionRepo: &mockAPISessionRepo{userID: 1},
|
||||
charRepo: &mockAPICharacterRepo{isNewResult: true},
|
||||
}
|
||||
|
||||
router := newTestRouter(server)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "/v2/characters/5", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-token")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("DELETE /v2/characters/5: status = %d, want 200", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2DeleteCharacter_DELETE_InvalidToken(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
sessionRepo: &mockAPISessionRepo{
|
||||
userIDErr: sql.ErrNoRows,
|
||||
},
|
||||
}
|
||||
|
||||
router := newTestRouter(server)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "/v2/characters/5", nil)
|
||||
req.Header.Set("Authorization", "Bearer bad-token")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("DELETE /v2/characters/5 (bad token): status = %d, want 401", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2DeleteCharacter_Finalized(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
sessionRepo: &mockAPISessionRepo{userID: 1},
|
||||
charRepo: &mockAPICharacterRepo{isNewResult: false},
|
||||
}
|
||||
|
||||
router := newTestRouter(server)
|
||||
|
||||
req := httptest.NewRequest("POST", "/v2/characters/5/delete", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-token")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("POST /v2/characters/5/delete (finalized): status = %d, want 200", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2ExportSave_InvalidToken(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
sessionRepo: &mockAPISessionRepo{
|
||||
userIDErr: sql.ErrNoRows,
|
||||
},
|
||||
}
|
||||
|
||||
router := newTestRouter(server)
|
||||
|
||||
req := httptest.NewRequest("GET", "/v2/characters/1/export", nil)
|
||||
req.Header.Set("Authorization", "Bearer bad-token")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("GET /v2/characters/1/export (bad token): status = %d, want 401", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2ExportSave_VerifyBody(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
sessionRepo: &mockAPISessionRepo{userID: 1},
|
||||
charRepo: &mockAPICharacterRepo{
|
||||
exportResult: map[string]interface{}{"name": "Hunter", "hr": float64(99)},
|
||||
},
|
||||
}
|
||||
|
||||
router := newTestRouter(server)
|
||||
|
||||
req := httptest.NewRequest("GET", "/v2/characters/1/export", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-token")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET /v2/characters/1/export: status = %d, want 200", rec.Code)
|
||||
}
|
||||
|
||||
var export ExportData
|
||||
if err := json.NewDecoder(rec.Body).Decode(&export); err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
if export.Character["name"] != "Hunter" {
|
||||
t.Errorf("character name = %v, want Hunter", export.Character["name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2CreateCharacter_DebugHR(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
conf := NewTestConfig()
|
||||
conf.DebugOptions.MaxLauncherHR = true
|
||||
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: conf,
|
||||
sessionRepo: &mockAPISessionRepo{userID: 1},
|
||||
charRepo: &mockAPICharacterRepo{
|
||||
newCharacter: Character{ID: 5, Name: "NewChar", HR: 999},
|
||||
},
|
||||
}
|
||||
|
||||
router := newTestRouter(server)
|
||||
|
||||
req := httptest.NewRequest("POST", "/v2/characters", nil)
|
||||
req.Header.Set("Authorization", "Bearer valid-token")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("POST /v2/characters: status = %d, want 200", rec.Code)
|
||||
}
|
||||
|
||||
var char Character
|
||||
if err := json.NewDecoder(rec.Body).Decode(&char); err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
if char.HR != 7 {
|
||||
t.Errorf("HR = %d, want 7 (capped by MaxLauncherHR)", char.HR)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyRoutesStillWork(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
server := &APIServer{
|
||||
|
||||
Reference in New Issue
Block a user