mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 11:34:35 +03:00
Refactor scanner presets, and add iran lion and sun flag
This commit is contained in:
@@ -42,6 +42,9 @@ jobs:
|
||||
goarch: amd64
|
||||
- goos: android
|
||||
goarch: arm64
|
||||
- goos: android
|
||||
goarch: arm
|
||||
goarm: '7'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
@@ -71,6 +74,7 @@ jobs:
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm || '' }}
|
||||
CGO_ENABLED: '0'
|
||||
run: |
|
||||
VERSION=${GITHUB_REF_NAME:-dev}
|
||||
@@ -111,7 +115,11 @@ jobs:
|
||||
- name: Stage Android client binary as JNI library
|
||||
run: |
|
||||
mkdir -p android/app/src/main/jniLibs/arm64-v8a
|
||||
mkdir -p android/app/src/main/jniLibs/armeabi-v7a
|
||||
test -f artifacts/thefeed-client-android-arm64
|
||||
test -f artifacts/thefeed-client-android-arm
|
||||
cp artifacts/thefeed-client-android-arm64 android/app/src/main/jniLibs/arm64-v8a/libthefeed.so
|
||||
cp artifacts/thefeed-client-android-arm android/app/src/main/jniLibs/armeabi-v7a/libthefeed.so
|
||||
|
||||
- name: Decode signing keystore
|
||||
env:
|
||||
@@ -143,7 +151,6 @@ jobs:
|
||||
gradle wrapper --gradle-version 8.10.2
|
||||
if [ -f app/keystore.jks ]; then BT=release; TASK=assembleRelease; else BT=debug; TASK=assembleDebug; fi
|
||||
|
||||
cp ../artifacts/thefeed-client-android-arm64 app/src/main/jniLibs/arm64-v8a/libthefeed.so
|
||||
./gradlew --no-daemon clean $TASK
|
||||
APK=$(find app/build/outputs/apk/$BT -name "*.apk" | head -1)
|
||||
cp "$APK" ../artifacts/thefeed-android-arm64.apk
|
||||
|
||||
@@ -44,7 +44,7 @@ clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
# Cross-compilation targets
|
||||
build-all: build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 build-freebsd-amd64 build-freebsd-arm64 build-windows-amd64 build-android-arm64
|
||||
build-all: build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 build-freebsd-amd64 build-freebsd-arm64 build-windows-amd64 build-android-arm64 build-android-arm
|
||||
|
||||
build-linux-amd64:
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@@ -85,6 +85,10 @@ build-android-arm64:
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
GOOS=android GOARCH=arm64 go build $(GOFLAGS) -o $(BUILD_DIR)/$(BINARY_CLIENT)-android-arm64 ./cmd/client
|
||||
|
||||
build-android-arm:
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
CGO_ENABLED=0 GOOS=android GOARCH=arm GOARM=7 go build $(GOFLAGS) -o $(BUILD_DIR)/$(BINARY_CLIENT)-android-arm ./cmd/client
|
||||
|
||||
# UPX compression (requires upx in PATH) — only for Linux/Windows binaries
|
||||
upx:
|
||||
@command -v upx >/dev/null 2>&1 || { echo "upx not found, skipping compression"; exit 0; }
|
||||
|
||||
+29
-16
@@ -116,6 +116,34 @@ sudo bash install.sh --login
|
||||
sudo bash install.sh --uninstall
|
||||
```
|
||||
|
||||
|
||||
> **توجه:** سرور باید روی پورت ۵۳ پاسخ بدهد. بهتر است روی پورت غیرمحدود (`:5300`) اجرا و با iptables فوروارد کنید:
|
||||
>
|
||||
> نام اینترفیس شبکه خود را با `ip a` پیدا کنید و `eth0` را جایگزین کنید:
|
||||
> ```bash
|
||||
> sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT
|
||||
> sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
> sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT
|
||||
> sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
> ```
|
||||
>
|
||||
> برای ماندگار کردن این قوانین بعد از ریبوت:
|
||||
> ```bash
|
||||
> sudo apt install iptables-persistent # Debian/Ubuntu
|
||||
> sudo netfilter-persistent save
|
||||
> ```
|
||||
|
||||
|
||||
**اگر مشکلی پیش آمد — حذف فوری redirect:**
|
||||
|
||||
```bash
|
||||
# حذف قانون iptables (بازگشت به حالت اولیه)
|
||||
sudo iptables -t nat -D PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
sudo iptables -D INPUT -p udp --dport 5300 -j ACCEPT
|
||||
sudo netfilter-persistent save
|
||||
```
|
||||
|
||||
|
||||
## 🐳 نصب با Docker (سرور)
|
||||
|
||||
اجرای سرور با Docker — بدون نیاز به نصب Go.
|
||||
@@ -275,21 +303,6 @@ chmod +x thefeed-client
|
||||
| A | `ns.example.com` | `203.0.113.10` |
|
||||
| NS | `t.example.com` | `ns.example.com` |
|
||||
|
||||
> **توجه:** سرور باید روی پورت ۵۳ پاسخ بدهد. بهتر است روی پورت غیرمحدود (`:5300`) اجرا و با iptables فوروارد کنید:
|
||||
>
|
||||
> نام اینترفیس شبکه خود را با `ip a` پیدا کنید و `eth0` را جایگزین کنید:
|
||||
> ```bash
|
||||
> sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT
|
||||
> sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
> sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT
|
||||
> sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
> ```
|
||||
>
|
||||
> برای ماندگار کردن این قوانین بعد از ریبوت:
|
||||
> ```bash
|
||||
> sudo apt install iptables-persistent # Debian/Ubuntu
|
||||
> sudo netfilter-persistent save
|
||||
> ```
|
||||
|
||||
## 🛠️ ساخت از سورس
|
||||
|
||||
@@ -364,7 +377,7 @@ MIT
|
||||
|
||||
<div align="center">
|
||||
|
||||
**برای ایران آزاد 🇮🇷**
|
||||
**برای ایران آزاد** <img src="internal/web/static/iran-lion-sun.svg" alt="شیر و خورشید" height="20">
|
||||
|
||||
*هر ایرانی حق دسترسی آزاد به اطلاعات را دارد*
|
||||
|
||||
|
||||
@@ -82,6 +82,34 @@ sudo bash -c "$(curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/mai
|
||||
Re-login: `curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/main/scripts/install.sh | sudo bash -s -- --login`
|
||||
Uninstall: `curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/main/scripts/install.sh | sudo bash -s -- --uninstall`
|
||||
|
||||
|
||||
> **Note:** The server needs to receive packets on external port 53. Running on `:53` directly requires root. It's better to listen on an unprivileged port (`:5300`) and port-forward 53 to it.
|
||||
>
|
||||
> Replace `eth0` with your actual network interface name (check with `ip a`):
|
||||
> ```bash
|
||||
> sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT
|
||||
> sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
> sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT
|
||||
> sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
> ```
|
||||
>
|
||||
> To make these rules persistent across reboots:
|
||||
> ```bash
|
||||
> sudo apt install iptables-persistent # Debian/Ubuntu
|
||||
> sudo netfilter-persistent save
|
||||
> ```
|
||||
|
||||
|
||||
|
||||
**If something goes wrong — remove the redirect instantly:**
|
||||
|
||||
```bash
|
||||
# Remove the iptables rule (restores original behavior)
|
||||
sudo iptables -t nat -D PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
sudo iptables -D INPUT -p udp --dport 5300 -j ACCEPT
|
||||
sudo netfilter-persistent save
|
||||
```
|
||||
|
||||
## Docker Deployment (Server)
|
||||
|
||||
Run the server with Docker — no Go toolchain needed.
|
||||
@@ -434,21 +462,6 @@ This points a hostname to your server IP.
|
||||
|
||||
This delegates all DNS queries for `t.example.com` (and its subdomains) to your server.
|
||||
|
||||
> **Note:** The server needs to receive packets on external port 53. Running on `:53` directly requires root. It's better to listen on an unprivileged port (`:5300`) and port-forward 53 to it.
|
||||
>
|
||||
> Replace `eth0` with your actual network interface name (check with `ip a`):
|
||||
> ```bash
|
||||
> sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT
|
||||
> sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
> sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT
|
||||
> sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300
|
||||
> ```
|
||||
>
|
||||
> To make these rules persistent across reboots:
|
||||
> ```bash
|
||||
> sudo apt install iptables-persistent # Debian/Ubuntu
|
||||
> sudo netfilter-persistent save
|
||||
> ```
|
||||
|
||||
## channels.txt Format
|
||||
|
||||
@@ -521,7 +534,7 @@ MIT
|
||||
|
||||
<div align="center">
|
||||
|
||||
**For FREE IRAN 🇮🇷**
|
||||
**For FREE IRAN** <img src="internal/web/static/iran-lion-sun.svg" alt="Lion-and-Sun" height="20">
|
||||
|
||||
*Everyone deserves free access to information*
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
@@ -53,6 +54,10 @@ class MainActivity : ComponentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Let the app draw behind the system status bar
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
// Force light (white) status bar icons on dark background
|
||||
val controller = WindowInsetsControllerCompat(window, window.decorView)
|
||||
controller.isAppearanceLightStatusBars = false
|
||||
controller.isAppearanceLightNavigationBars = false
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// Apply top inset as padding so content isn't hidden behind the status bar
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
<item name="android:statusBarColor" tools:targetApi="l">@color/bg</item>
|
||||
<item name="android:navigationBarColor">@color/bg</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:windowBackground">@color/bg</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -178,20 +178,26 @@ func (f *Fetcher) SetResolvers(resolvers []string) {
|
||||
copy(f.activeResolvers, resolvers)
|
||||
}
|
||||
|
||||
// UpdateResolverPool replaces the full resolver list but keeps the existing
|
||||
// active pool intact (only pruning resolvers that are no longer in the bank).
|
||||
// New bank entries are added to allResolvers but NOT automatically activated.
|
||||
// UpdateResolverPool replaces the full resolver list and removes any active
|
||||
// resolvers that are no longer in the bank.
|
||||
func (f *Fetcher) UpdateResolverPool(resolvers []string) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
bankSet := make(map[string]bool, len(resolvers))
|
||||
for _, r := range resolvers {
|
||||
bankSet[r] = true
|
||||
k := r
|
||||
if !strings.Contains(k, ":") {
|
||||
k += ":53"
|
||||
}
|
||||
bankSet[k] = true
|
||||
}
|
||||
// Prune active resolvers that were removed from the bank.
|
||||
filtered := make([]string, 0, len(f.activeResolvers))
|
||||
for _, r := range f.activeResolvers {
|
||||
if bankSet[r] {
|
||||
k := r
|
||||
if !strings.Contains(k, ":") {
|
||||
k += ":53"
|
||||
}
|
||||
if bankSet[k] {
|
||||
filtered = append(filtered, r)
|
||||
}
|
||||
}
|
||||
|
||||
+26
-3
@@ -14,15 +14,32 @@ func (s *Server) handleScannerPresets(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "method not allowed", 405)
|
||||
return
|
||||
}
|
||||
src := defaultScannerPresets
|
||||
type preset struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
writeJSON(w, map[string]any{
|
||||
"presets": []preset{
|
||||
{Name: "ir", Label: "Iran", Count: parseScannerPresetCount()},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// parseScannerPresetLines returns the parsed non-empty, non-comment lines from the preset.
|
||||
func parseScannerPresetLines() []string {
|
||||
var lines []string
|
||||
for _, line := range strings.Split(src, "\n") {
|
||||
for _, line := range strings.Split(defaultScannerPresets, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
writeJSON(w, lines)
|
||||
return lines
|
||||
}
|
||||
|
||||
func parseScannerPresetCount() int {
|
||||
return len(parseScannerPresetLines())
|
||||
}
|
||||
|
||||
func (s *Server) handleScannerStart(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -33,6 +50,7 @@ func (s *Server) handleScannerStart(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var req struct {
|
||||
Targets []string `json:"targets"`
|
||||
Preset string `json:"preset"` // e.g. "ir" — server-side preset, avoids sending 50K IPs
|
||||
MaxIPs int `json:"maxIPs"`
|
||||
RateLimit int `json:"rateLimit"`
|
||||
Timeout float64 `json:"timeout"`
|
||||
@@ -45,6 +63,11 @@ func (s *Server) handleScannerStart(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve preset into targets server-side.
|
||||
if req.Preset == "ir" && len(req.Targets) == 0 {
|
||||
req.Targets = parseScannerPresetLines()
|
||||
}
|
||||
|
||||
if len(req.Targets) == 0 {
|
||||
http.Error(w, "targets required", 400)
|
||||
return
|
||||
|
||||
@@ -1762,10 +1762,11 @@
|
||||
<label data-i18n="scanner_targets">IPs or CIDRs (one per line)</label>
|
||||
<div style="display:flex;gap:4px">
|
||||
<button class="btn btn-flat" onclick="document.getElementById('scanTargets').value=''" data-i18n="scanner_clear_targets" style="font-size:12px;padding:4px 10px">🗑 Clear</button>
|
||||
<button class="btn btn-flat" onclick="loadScannerPresets()" data-i18n="scanner_load_presets" style="font-size:12px;padding:4px 10px">🇮🇷 IR</button>
|
||||
<button class="btn btn-flat" onclick="loadScannerPresets()" style="font-size:12px;padding:4px 10px"><img class="iran-flag-icon" src="/static/iran-lion-sun.svg" alt="IR" style="height:14px;vertical-align:middle;margin-right:2px"> <span data-i18n="scanner_load_presets">Load IR Presets</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="scanTargets" rows="3" placeholder="5.1.0.0/16 8.8.8.8 1.1.1.1" style="width:100%;font-family:monospace;font-size:13px"></textarea>
|
||||
<div id="scanPresetTag" style="display:none;margin-top:6px;padding:6px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;font-size:13px;color:var(--text)"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="scanner_profile">Profile</label>
|
||||
@@ -1939,7 +1940,8 @@
|
||||
scanner_about_short: 'بازههای IP را اسکن کنید تا ریزالورهای DNS سازگار با سرور شما پیدا شوند.',
|
||||
scanner_read_more: 'بیشتر بخوانید...',
|
||||
scanner_read_less: 'بستن',
|
||||
scanner_load_presets: '\uD83C\uDDEE\uD83C\uDDF7 بارگذاری لیست ایران',
|
||||
scanner_load_presets: 'بارگذاری لیست ایران',
|
||||
scanner_preset_active: 'ریزالورهای ایران بارگذاری شد',
|
||||
scanner_new_scan: 'اسکن جدید',
|
||||
scanner_advanced: 'تنظیمات پیشرفته',
|
||||
scanner_copy_all: 'کپی همه',
|
||||
@@ -2070,7 +2072,8 @@
|
||||
scanner_about_short: 'Scan IP ranges to find DNS resolvers that work with your server.',
|
||||
scanner_read_more: 'Read more...',
|
||||
scanner_read_less: 'Show less',
|
||||
scanner_load_presets: '\uD83C\uDDEE\uD83C\uDDF7 Load IR Presets',
|
||||
scanner_load_presets: 'Load IR Presets',
|
||||
scanner_preset_active: 'Iran resolvers loaded',
|
||||
scanner_new_scan: 'New Scan',
|
||||
scanner_advanced: 'Advanced options',
|
||||
scanner_copy_all: 'Copy All',
|
||||
@@ -2961,7 +2964,7 @@
|
||||
var timeStr = ts.toLocaleTimeString(dateLocale, { hour: '2-digit', minute: '2-digit' });
|
||||
var text = msg.Text || msg.text || '';
|
||||
currentMsgTexts.push(text);
|
||||
var mediaHtml = '', textHtml = esc(text);
|
||||
var mediaHtml = '', textHtml = esc(text).replace(/\uD83C\uDDEE\uD83C\uDDF7/g, '<img src="/static/iran-lion-sun.svg" alt="\u{1F981}\u2600\uFE0F" style="height:1.1em;vertical-align:middle">');
|
||||
var mediaTypes = ['[IMAGE]', '[VIDEO]', '[FILE]', '[AUDIO]', '[STICKER]', '[GIF]', '[POLL]', '[CONTACT]', '[LOCATION]', '[REPLY]'];
|
||||
for (var m = 0; m < mediaTypes.length; m++) {
|
||||
if (text.indexOf(mediaTypes[m]) === 0) {
|
||||
@@ -3291,6 +3294,7 @@
|
||||
// ===== SCANNER =====
|
||||
var scanPollTimer = null;
|
||||
var scanLastResults = []; // cache for selection
|
||||
var scannerActivePreset = ''; // server-side preset name (e.g. 'ir')
|
||||
|
||||
function openScanner() {
|
||||
document.getElementById('scannerModal').classList.add('active');
|
||||
@@ -3318,27 +3322,44 @@
|
||||
}
|
||||
|
||||
async function loadScannerPresets() {
|
||||
if (scannerActivePreset === 'ir') {
|
||||
// Toggle off
|
||||
scannerActivePreset = '';
|
||||
renderPresetTag();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var r = await fetch('/api/scanner/presets');
|
||||
if (!r.ok) return;
|
||||
var lines = await r.json();
|
||||
if (lines && lines.length) {
|
||||
var el = document.getElementById('scanTargets');
|
||||
var existing = el.value.trim();
|
||||
el.value = existing ? existing + '\n' + lines.join('\n') : lines.join('\n');
|
||||
var data = await r.json();
|
||||
var presets = data.presets || [];
|
||||
if (presets.length > 0) {
|
||||
scannerActivePreset = presets[0].name;
|
||||
renderPresetTag();
|
||||
}
|
||||
} catch (e) { showToast(e.message) }
|
||||
}
|
||||
function renderPresetTag() {
|
||||
var tag = document.getElementById('scanPresetTag');
|
||||
if (scannerActivePreset) {
|
||||
tag.style.display = '';
|
||||
tag.innerHTML = '<img src="/static/iran-lion-sun.svg" alt="IR" style="height:14px;vertical-align:middle;margin-right:4px"> ' + t('scanner_preset_active');
|
||||
} else {
|
||||
tag.style.display = 'none';
|
||||
tag.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function startScan() {
|
||||
var targets = document.getElementById('scanTargets').value.trim().split('\n').filter(function (s) { return s.trim() });
|
||||
if (!targets.length) { showToast(t('scanner_targets')); return }
|
||||
if (!targets.length && !scannerActivePreset) { showToast(t('scanner_targets')); return }
|
||||
// Clear stale results from previous scan.
|
||||
scanLastResults = [];
|
||||
document.getElementById('scanResultsBody').innerHTML = '';
|
||||
document.getElementById('scannerApplySection').style.display = 'none';
|
||||
var body = {
|
||||
targets: targets,
|
||||
preset: scannerActivePreset || undefined,
|
||||
profileId: document.getElementById('scanProfile').value,
|
||||
rateLimit: parseInt(document.getElementById('scanRateLimit').value) || 50,
|
||||
timeout: parseInt(document.getElementById('scanTimeout').value) || 15,
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 40 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 40 KiB |
@@ -165,11 +165,23 @@ func TestE2E_Scanner_Presets(t *testing.T) {
|
||||
t.Fatalf("GET /api/scanner/presets: expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var lines []string
|
||||
if err := json.NewDecoder(resp.Body).Decode(&lines); err != nil {
|
||||
var data struct {
|
||||
Presets []struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Count int `json:"count"`
|
||||
} `json:"presets"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
if len(data.Presets) == 0 {
|
||||
t.Error("expected non-empty presets list")
|
||||
}
|
||||
if data.Presets[0].Name != "ir" {
|
||||
t.Errorf("first preset name = %q, want ir", data.Presets[0].Name)
|
||||
}
|
||||
if data.Presets[0].Count == 0 {
|
||||
t.Error("expected non-zero count for ir preset")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user