Add CPONE lab and radiology result forwarding

This commit is contained in:
2026-10-01 14:01:32 +07:00
parent 0d2044926e
commit 102823c2b2
6 changed files with 201 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
package http
import (
"bytes"
"encoding/json"
"io"
"mime"
"net/http"
"time"
)
const maxUpdateResultBody = 8 << 20
type resultHTTPClient interface {
Do(*http.Request) (*http.Response, error)
}
type updateResultForwarder struct {
targetURL string
client resultHTTPClient
}
// NewUpdateResultForwarder relays the original JSON body to CPONE without
// forwarding the caller's Authorization header or cookies.
func NewUpdateResultForwarder(targetURL string, client resultHTTPClient) http.Handler {
if client == nil {
client = &http.Client{Timeout: 30 * time.Second}
}
return updateResultForwarder{
targetURL: targetURL,
client: client,
}
}
func (h updateResultForwarder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || mediaType != "application/json" {
WriteJSON(w, http.StatusUnsupportedMediaType, Response{Success: false, Message: "Content-Type harus application/json"})
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, maxUpdateResultBody+1))
if err != nil {
WriteJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Gagal membaca payload hasil pemeriksaan"})
return
}
if len(body) > maxUpdateResultBody {
WriteJSON(w, http.StatusRequestEntityTooLarge, Response{Success: false, Message: "Payload hasil pemeriksaan terlalu besar"})
return
}
var payload map[string]json.RawMessage
if err := json.Unmarshal(body, &payload); err != nil || payload == nil {
WriteJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Payload harus berupa object JSON"})
return
}
upstreamRequest, err := http.NewRequestWithContext(r.Context(), http.MethodPost, h.targetURL, bytes.NewReader(body))
if err != nil {
WriteJSON(w, http.StatusBadGateway, Response{Success: false, Message: "URL tujuan CPONE tidak valid"})
return
}
upstreamRequest.Header.Set("Content-Type", "application/json")
upstreamRequest.Header.Set("Accept", "application/json")
upstreamResponse, err := h.client.Do(upstreamRequest)
if err != nil {
WriteJSON(w, http.StatusBadGateway, Response{Success: false, Message: "Gagal menghubungi API hasil pemeriksaan CPONE"})
return
}
defer upstreamResponse.Body.Close()
if contentType := upstreamResponse.Header.Get("Content-Type"); contentType != "" {
w.Header().Set("Content-Type", contentType)
}
w.WriteHeader(upstreamResponse.StatusCode)
_, _ = io.Copy(w, upstreamResponse.Body)
}

View File

@@ -0,0 +1,96 @@
package http
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type fakeResultClient func(*http.Request) (*http.Response, error)
func (f fakeResultClient) Do(r *http.Request) (*http.Response, error) { return f(r) }
func TestUpdateResultForwarderPreservesPayloadAndResponse(t *testing.T) {
const body = `{"kode_rs":"GM","no_regpas":"MCU260900031","results":[{"layanan_id":"1102","result":"5","unit":"mm\/jam"}]}`
called := false
client := fakeResultClient(func(r *http.Request) (*http.Response, error) {
called = true
if r.Method != http.MethodPost || r.URL.Host != "devcpone.aplikasi.web.id" || r.URL.Path != "/one-api/api_his/update_result" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
}
gotBody, _ := io.ReadAll(r.Body)
if string(gotBody) != body {
t.Errorf("payload changed: %s", gotBody)
}
if r.Header.Get("Authorization") != "" || r.Header.Get("Cookie") != "" {
t.Error("caller credentials were forwarded")
}
return &http.Response{
StatusCode: http.StatusUnprocessableEntity,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"Status":{"OK":false,"Messages":"ORDER_STORED_PENDING_MATCH"}}`)),
}, nil
})
handler := NewUpdateResultForwarder("https://devcpone.aplikasi.web.id/one-api/api_his/update_result", client)
r := httptest.NewRequest(http.MethodPost, "/api/cpone/his/update-result", strings.NewReader(body))
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Authorization", "Bearer middleware-secret")
r.Header.Set("Cookie", "session=secret")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if !called || w.Code != http.StatusUnprocessableEntity || !strings.Contains(w.Body.String(), "ORDER_STORED_PENDING_MATCH") {
t.Fatalf("called=%t status=%d body=%s", called, w.Code, w.Body.String())
}
}
func TestUpdateResultForwarderRejectsInvalidJSON(t *testing.T) {
called := false
client := fakeResultClient(func(r *http.Request) (*http.Response, error) {
called = true
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("ok"))}, nil
})
handler := NewUpdateResultForwarder("https://devcpone.aplikasi.web.id/one-api/api_his/update_result", client)
r := httptest.NewRequest(http.MethodPost, "/api/cpone/his/update-result", strings.NewReader(`{"kode_rs":`))
r.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusBadRequest || called {
t.Fatalf("status=%d upstream_called=%t", w.Code, called)
}
}
func TestRadiologyResultForwarderPreservesPayloadAndResponse(t *testing.T) {
const body = `{"kode_rs":"GM","no_regpas":"MCU260900014","medrec":"00640052","dokter_id":"DR00069","results":[{"layanan_id":"RAD-01-01-001","testName":"A103 - Thorax PA","modality":"CR","expertise":"Hasil ekspertisi"}]}`
called := false
client := fakeResultClient(func(r *http.Request) (*http.Response, error) {
called = true
if r.Method != http.MethodPost || r.URL.Host != "devcpone.aplikasi.web.id" || r.URL.Path != "/one-api/api_his/update_result_radiologi" {
t.Errorf("unexpected request: %s %s", r.Method, r.URL.String())
}
gotBody, _ := io.ReadAll(r.Body)
if string(gotBody) != body {
t.Errorf("radiology payload changed: %s", gotBody)
}
if r.Header.Get("Authorization") != "" || r.Header.Get("Cookie") != "" {
t.Error("caller credentials were forwarded")
}
return &http.Response{
StatusCode: http.StatusUnprocessableEntity,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"Status":{"OK":false,"Messages":"dokter_id HIS belum memiliki mapping dokter CPONE [Radiology004]"}}`)),
}, nil
})
handler := NewUpdateResultForwarder("https://devcpone.aplikasi.web.id/one-api/api_his/update_result_radiologi", client)
r := httptest.NewRequest(http.MethodPost, "/api/cpone/his/update-result-radiologi", strings.NewReader(body))
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Authorization", "Bearer middleware-secret")
r.Header.Set("Cookie", "session=secret")
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if !called || w.Code != http.StatusUnprocessableEntity || !strings.Contains(w.Body.String(), "Radiology004") {
t.Fatalf("called=%t status=%d body=%s", called, w.Code, w.Body.String())
}
}