ownership: add IDX above1 discovery and import

Refactor KSEI PDF parsing for the live above-1 holder-register layout, add IDX announcement discovery plus browser-impersonated PDF fetches, and update the ownership docs to mark Batch 1 complete and scope Batch 2 around above1 hardening and unsupported legacy inputs.
This commit is contained in:
Rasyidan Akbar F. 2026-03-30 11:57:14 +07:00
commit a36648fd89
13 changed files with 1716 additions and 464 deletions

View file

@ -1,5 +1,8 @@
use std::fs;
use std::io::{Read, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::thread;
use assert_cmd::Command;
use predicates::prelude::*;
@ -38,6 +41,30 @@ fn test_bin(name: &str) -> Command {
bin_with_root(&root)
}
fn spawn_single_response_server(content_type: &str, body: impl Into<Vec<u8>>) -> String {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local test server");
let addr = listener.local_addr().expect("local addr");
let content_type = content_type.to_string();
let body = body.into();
thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept test connection");
let mut buf = [0u8; 2048];
let _ = stream.read(&mut buf);
let headers = format!(
"HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
stream
.write_all(headers.as_bytes())
.expect("write response headers");
stream.write_all(&body).expect("write response body");
});
format!("http://{addr}")
}
#[test]
fn help_works() {
test_bin("help").arg("--help").assert().success();
@ -580,6 +607,108 @@ fn ownership_import_fetch_bing_reports_unsupported() {
));
}
#[test]
fn ownership_discover_lists_fixture_candidates() {
let body = fs::read_to_string("tests/fixtures/idx_announcement_kepemilikan.json")
.expect("read ownership discovery fixture");
let json_url = spawn_single_response_server("application/json", body);
test_bin("ownership-discover")
.env("IDX_CURL_IMPERSONATE_BIN", "curl")
.env("IDX_OWNERSHIP_ANNOUNCEMENT_API_URL", &json_url)
.env(
"IDX_OWNERSHIP_ANNOUNCEMENT_PAGE_URL",
"http://127.0.0.1/pengumuman",
)
.args([
"ownership",
"discover",
"--family",
"above5",
"--limit",
"2",
])
.assert()
.success()
.stdout(predicate::str::contains("Above 5%"))
.stdout(predicate::str::contains(
"20260327_Semua Emiten Saham_Pengumuman Bursa_32055594.pdf",
))
.stdout(predicate::str::contains(
"20260327_Semua Emiten Saham_Pengumuman Bursa_32055594_lamp1.pdf",
));
}
#[test]
fn ownership_discover_supports_above1_family() {
let body = r#"{
"Items": [
{
"PublishDate": "2026-03-10T12:09:09",
"Title": "Pemegang Saham di atas 1% (KSEI)",
"AnnouncementType": "",
"Code": "Semua Emiten Saham",
"Attachments": [
{
"PDFFilename": "d67ebf37e6_10d4080288.pdf",
"FullSavePath": "https://www.idx.co.id/StaticData/NewsAndAnnouncement/ANNOUNCEMENTSTOCK/From_EREP/202603/d67ebf37e6_10d4080288.pdf",
"IsAttachment": 0,
"OriginalFilename": "20260310_Semua Emiten Saham_Pengumuman Bursa_32052554.pdf"
},
{
"PDFFilename": "b9b638e5a8_8928aca255.pdf",
"FullSavePath": "https://www.idx.co.id/StaticData/NewsAndAnnouncement/ANNOUNCEMENTSTOCK/From_EREP/202603/b9b638e5a8_8928aca255.pdf",
"IsAttachment": 1,
"OriginalFilename": "20260310_Semua Emiten Saham_Pengumuman Bursa_32052554_lamp1.pdf"
}
],
"PdfPath": ""
}
],
"ItemCount": 1,
"PageSize": 10,
"PageNumber": 1,
"PageCount": 1
}"#;
let json_url = spawn_single_response_server("application/json", body.to_string());
test_bin("ownership-discover-above1")
.env("IDX_CURL_IMPERSONATE_BIN", "curl")
.env("IDX_OWNERSHIP_ANNOUNCEMENT_API_URL", &json_url)
.env(
"IDX_OWNERSHIP_ANNOUNCEMENT_PAGE_URL",
"http://127.0.0.1/pengumuman",
)
.args([
"ownership",
"discover",
"--family",
"above1",
"--limit",
"2",
])
.assert()
.success()
.stdout(predicate::str::contains("Above 1%"))
.stdout(predicate::str::contains(
"20260310_Semua Emiten Saham_Pengumuman Bursa_32052554_lamp1.pdf",
));
}
#[test]
fn ownership_import_url_rejects_html_response_before_pdf_parse() {
let html_url = spawn_single_response_server(
"text/html; charset=utf-8",
"<!doctype html><html><body>blocked</body></html>",
);
test_bin("ownership-import-url-html")
.args(["ownership", "import", "--url", &html_url])
.assert()
.failure()
.stderr(predicate::str::contains("returned HTML instead of a PDF"));
}
#[test]
fn technical_serves_stale_cache_on_provider_failure_with_warning() {
let root = test_env_dir("technical-stale");

View file

@ -0,0 +1,48 @@
{
"Items": [
{
"Id": "20260327163420-Peng-LKS-00043/BEI.PLP/03-2026_id-id",
"AnnouncementNo": "Peng-LKS-00043/BEI.PLP/03-2026",
"PublishDate": "2026-03-27T16:34:20",
"Title": "Pemegang Saham di atas 5% (KSEI)",
"AnnouncementType": "",
"Code": "Semua Emiten Saham",
"Attachments": [
{
"PDFFilename": "20260327_Semua Emiten Saham_Pengumuman Bursa_32055594.pdf",
"FullSavePath": "https://www.idx.co.id/StaticData/NewsAndAnnouncement/ANNOUNCEMENTSTOCK/From_EREP/202603/5d31bb6f49_announcement.pdf",
"IsAttachment": 0,
"OriginalFilename": "20260327_Semua Emiten Saham_Pengumuman Bursa_32055594.pdf"
},
{
"PDFFilename": "20260327_Semua Emiten Saham_Pengumuman Bursa_32055594_lamp1.pdf",
"FullSavePath": "https://www.idx.co.id/StaticData/NewsAndAnnouncement/ANNOUNCEMENTSTOCK/From_EREP/202603/4f5c4efc6f_bf70f249ac_lamp1.pdf",
"IsAttachment": 1,
"OriginalFilename": "20260327_Semua Emiten Saham_Pengumuman Bursa_32055594_lamp1.pdf"
}
],
"PdfPath": "[{\"PDFFilename\":\"20260327_Semua Emiten Saham_Pengumuman Bursa_32055594.pdf\",\"FullSavePath\":\"https://www.idx.co.id/StaticData/NewsAndAnnouncement/ANNOUNCEMENTSTOCK/From_EREP/202603/5d31bb6f49_announcement.pdf\",\"IsAttachment\":\"0\",\"OriginalFilename\":\"20260327_Semua Emiten Saham_Pengumuman Bursa_32055594.pdf\"},{\"PDFFilename\":\"20260327_Semua Emiten Saham_Pengumuman Bursa_32055594_lamp1.pdf\",\"FullSavePath\":\"https://www.idx.co.id/StaticData/NewsAndAnnouncement/ANNOUNCEMENTSTOCK/From_EREP/202603/4f5c4efc6f_bf70f249ac_lamp1.pdf\",\"IsAttachment\":\"1\",\"OriginalFilename\":\"20260327_Semua Emiten Saham_Pengumuman Bursa_32055594_lamp1.pdf\"}]"
},
{
"Id": "20260328114739-LK/28032026/0001/1_id-id",
"AnnouncementNo": "LK/28032026/0001/1",
"PublishDate": "2026-03-28T11:47:39",
"Title": "LAPORAN KEPEMILIKAN ATAU SETIAP PERUBAHAN KEPEMILIKAN SAHAM PERUSAHAAN TERBUKA",
"AnnouncementType": "LKS",
"Code": "CASH",
"Attachments": [
{
"PDFFilename": "LK-28032026-2468-00.pdf-0",
"FullSavePath": "https://www.idx.co.id/StaticData/NewsAndAnnouncement/ANNOUNCEMENTSTOCK/From_KSEI/LK-28032026-2468-00.pdf-0.pdf",
"IsAttachment": 1,
"OriginalFilename": "LK-28032026-2468-00.pdf"
}
],
"PdfPath": "[{\"PDFFilename\":\"LK-28032026-2468-00.pdf-0\",\"FullSavePath\":\"https://www.idx.co.id/StaticData/NewsAndAnnouncement/ANNOUNCEMENTSTOCK/From_KSEI/LK-28032026-2468-00.pdf-0.pdf\",\"IsAttachment\":1,\"OriginalFilename\":\"LK-28032026-2468-00.pdf\"}]"
}
],
"ItemCount": 2,
"PageSize": 10,
"PageNumber": 1,
"PageCount": 1
}