one-for-all-framework 5.1.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/Procfile +1 -1
- data/README.md +1 -1
- data/app/controllers/auth_controller.rb +2 -0
- data/app/controllers/products_controller.rb +6 -1
- data/app/controllers/projects_controller.rb +4 -0
- data/app/views/docs.erb +1 -1
- data/app/views/index.erb +1 -1
- data/app/views/layout.erb +90 -23
- data/app/views/page.erb +11 -5
- data/app/views/post.erb +13 -10
- data/app/views/project.erb +18 -12
- data/bin/ofa +26 -6
- data/config/boot.rb +10 -5
- data/config/features.json +2 -2
- data/db/data.sqlite3 +0 -0
- data/db/migrations/20260507000000_create_activity_logs.rb +1 -1
- data/test/features_test.rb +85 -0
- data/test/framework_spec.rb +51 -0
- data/test/ofa_spec.rb +205 -0
- data/test/test_helper.rb +13 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 997b63198052b18b0a1f1f74d74829835a408e0efa5b2adc38e795e9753a9576
|
|
4
|
+
data.tar.gz: 7dfe59789c4cef94ab2a49add1d459f3ba2cf1c362ab812238e74643019eb907
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4d233bc2974284d2ba9de83f20eb4f900bc70fbae6bdf1fc58dc2d9ca8348dcfb6d9baf675347d002dca1620d9d47d9313e44dd0fcda36ae11a66d7ee7ec6a1
|
|
7
|
+
data.tar.gz: 8d634a30437413830ce93d54daf1a3dbed0f27cb1ff46e88fe1bc903435048f0873cdba4899e5f7e1e2933bcb318cc677c1d682a6be551067bcb13a3d67074d7
|
data/Dockerfile
CHANGED
data/Procfile
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
web:
|
|
1
|
+
web: bundle exec ofa run
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="public/images/logo.png" width="500" height="500" alt="OFA Framework Logo">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
# ⚡ One-For-All (OFA) Framework v5.
|
|
5
|
+
# ⚡ One-For-All (OFA) Framework v5.3.0
|
|
6
6
|
|
|
7
7
|
[](https://www.ruby-lang.org/)
|
|
8
8
|
[](LICENSE)
|
|
@@ -22,6 +22,7 @@ class AuthController < ApplicationController
|
|
|
22
22
|
session['username'] = user.username
|
|
23
23
|
session['last_active_at'] = Time.now.to_i
|
|
24
24
|
|
|
25
|
+
log_activity("User logged in: #{user.username}")
|
|
25
26
|
redirect_to '/dashboard'
|
|
26
27
|
else
|
|
27
28
|
render 'login', title: 'Login - One-For-All', error: 'Invalid credentials'
|
|
@@ -29,6 +30,7 @@ class AuthController < ApplicationController
|
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def logout
|
|
33
|
+
log_activity("User logged out: #{session['username']}")
|
|
32
34
|
['user_id', :user_id, 'username', :username, 'last_active_at', :last_active_at].each { |k| session.delete(k) }
|
|
33
35
|
redirect_to '/login'
|
|
34
36
|
end
|
|
@@ -20,6 +20,7 @@ class ProductsController < ApplicationController
|
|
|
20
20
|
data = params['product']
|
|
21
21
|
@product = Product.new(data)
|
|
22
22
|
if @product.save
|
|
23
|
+
log_activity("Created product: #{@product.name}", @product, "Price: #{@product.price}")
|
|
23
24
|
redirect_to '/dashboard/products'
|
|
24
25
|
else
|
|
25
26
|
render 'cms/products_form'
|
|
@@ -33,7 +34,9 @@ class ProductsController < ApplicationController
|
|
|
33
34
|
|
|
34
35
|
def update
|
|
35
36
|
@product = Product[params['id']]
|
|
36
|
-
|
|
37
|
+
data = params['product']
|
|
38
|
+
if @product.update(data)
|
|
39
|
+
log_activity("Updated product: #{@product.name}", @product, "New Data: #{data.to_json}")
|
|
37
40
|
redirect_to '/dashboard/products'
|
|
38
41
|
else
|
|
39
42
|
render 'cms/products_form'
|
|
@@ -42,7 +45,9 @@ class ProductsController < ApplicationController
|
|
|
42
45
|
|
|
43
46
|
def destroy
|
|
44
47
|
@product = Product[params['id']]
|
|
48
|
+
name = @product.name
|
|
45
49
|
@product.destroy
|
|
50
|
+
log_activity("Deleted product: #{name}")
|
|
46
51
|
redirect_to '/dashboard/products'
|
|
47
52
|
end
|
|
48
53
|
|
|
@@ -16,6 +16,7 @@ class ProjectsController < ApplicationController
|
|
|
16
16
|
data['is_active'] = boolean_param(data['is_active'])
|
|
17
17
|
@project = Project.new(data)
|
|
18
18
|
if @project.save
|
|
19
|
+
log_activity("Created project: #{@project.title}", @project, "Link: #{@project.link}")
|
|
19
20
|
redirect_to '/dashboard/projects'
|
|
20
21
|
else
|
|
21
22
|
render 'cms/projects_form'
|
|
@@ -32,6 +33,7 @@ class ProjectsController < ApplicationController
|
|
|
32
33
|
data = params['project']
|
|
33
34
|
data['is_active'] = boolean_param(data['is_active'])
|
|
34
35
|
if @project.update(data)
|
|
36
|
+
log_activity("Updated project: #{@project.title}", @project, "New Data: #{data.to_json}")
|
|
35
37
|
redirect_to '/dashboard/projects'
|
|
36
38
|
else
|
|
37
39
|
render 'cms/projects_form'
|
|
@@ -42,7 +44,9 @@ class ProjectsController < ApplicationController
|
|
|
42
44
|
@project = Project[params['id']]
|
|
43
45
|
delete_from_storage(@project.image_url) if @project.respond_to?(:image_url)
|
|
44
46
|
delete_all_images_from_content(@project.description) if @project.respond_to?(:description)
|
|
47
|
+
title = @project.title
|
|
45
48
|
@project.destroy
|
|
49
|
+
log_activity("Deleted project: #{title}")
|
|
46
50
|
redirect_to '/dashboard/projects'
|
|
47
51
|
end
|
|
48
52
|
end
|
data/app/views/docs.erb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="space-y-12 pb-20">
|
|
2
2
|
<div class="text-left">
|
|
3
|
-
<div class="badge-premium">Documentation v5.
|
|
3
|
+
<div class="badge-premium">Documentation v5.3.0</div>
|
|
4
4
|
<h1 class="text-5xl font-black tracking-tighter mb-4 text-slate-700 dark:text-white">Framework <span class="text-primary">Guide</span></h1>
|
|
5
5
|
<p class="text-xl text-slate-500 max-w-2xl leading-relaxed">Everything you need to know about building premium web applications with the One-For-All framework.</p>
|
|
6
6
|
</div>
|
data/app/views/index.erb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="text-center space-y-6 max-w-2xl mx-auto py-12">
|
|
2
2
|
<div class="inline-flex items-center px-4 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider animate-pulse">
|
|
3
|
-
Framework Version 5.
|
|
3
|
+
Framework Version 5.3.0
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<h1 class="text-5xl md:text-7xl font-black tracking-tight leading-tight">
|
data/app/views/layout.erb
CHANGED
|
@@ -283,33 +283,100 @@
|
|
|
283
283
|
|
|
284
284
|
.nav-item {
|
|
285
285
|
color: #64748b;
|
|
286
|
-
font-weight:
|
|
287
|
-
transition:
|
|
286
|
+
font-weight: 700;
|
|
287
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
288
|
+
position: relative;
|
|
289
|
+
padding: 0.5rem 0;
|
|
290
|
+
display: inline-flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.nav-item::after {
|
|
295
|
+
content: '';
|
|
296
|
+
position: absolute;
|
|
297
|
+
bottom: 0;
|
|
298
|
+
left: 0;
|
|
299
|
+
width: 0;
|
|
300
|
+
height: 2px;
|
|
301
|
+
background: linear-gradient(90deg, var(--primary), var(--secondary));
|
|
302
|
+
transition: width 0.3s ease;
|
|
303
|
+
border-radius: 99px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.nav-item:hover::after, .nav-item.active::after {
|
|
307
|
+
width: 100%;
|
|
288
308
|
}
|
|
289
309
|
|
|
290
310
|
.nav-item:hover, .nav-item.active {
|
|
291
311
|
color: var(--primary);
|
|
312
|
+
transform: translateY(-1px);
|
|
292
313
|
}
|
|
293
314
|
|
|
294
315
|
/* Markdown Overrides */
|
|
295
|
-
.markdown-content { text-align: left; line-height: 1.
|
|
296
|
-
.markdown-content h1 { font-size: 2.
|
|
297
|
-
.markdown-content
|
|
316
|
+
.markdown-content { text-align: left; line-height: 1.8; color: #475569; overflow-x: auto; }
|
|
317
|
+
.markdown-content h1 { font-size: 2.5rem; font-weight: 900; margin: 2.5rem 0 1.5rem; color: var(--primary); letter-spacing: -0.02em; line-height: 1.2; }
|
|
318
|
+
.markdown-content h2 { font-size: 2rem; font-weight: 800; margin: 2.5rem 0 1.25rem; color: var(--primary); letter-spacing: -0.01em; border-bottom: 1px solid rgba(0,0,0,0.05); padding-bottom: 0.5rem; }
|
|
319
|
+
.markdown-content h3 { font-size: 1.5rem; font-weight: 800; margin: 2rem 0 1rem; color: var(--secondary); }
|
|
320
|
+
.markdown-content h4 { font-size: 1.25rem; font-weight: 700; margin: 1.5rem 0 0.75rem; color: var(--primary); }
|
|
321
|
+
.markdown-content p { font-size: 1.1rem; margin-bottom: 1.5rem; }
|
|
298
322
|
.markdown-content ul { list-style-type: disc !important; margin-left: 1.5rem !important; margin-bottom: 1.5rem !important; display: block !important; }
|
|
299
323
|
.markdown-content ol { list-style-type: decimal !important; margin-left: 1.5rem !important; margin-bottom: 1.5rem !important; display: block !important; }
|
|
300
324
|
.markdown-content li { margin-bottom: 0.5rem !important; display: list-item !important; }
|
|
301
|
-
.markdown-content blockquote { border-left: 4px solid var(--primary); padding: 1rem
|
|
302
|
-
.markdown-content pre { background:
|
|
303
|
-
.markdown-content code { background: rgba(0,0,0,0.05); padding: 0.2rem 0.4rem; border-radius: 0.4rem; font-family: 'Fira Code', monospace; font-size: 0.9em; }
|
|
304
|
-
.markdown-content
|
|
305
|
-
.markdown-content
|
|
306
|
-
.markdown-content
|
|
325
|
+
.markdown-content blockquote { border-left: 4px solid var(--primary); padding: 1rem 1.5rem; font-style: italic; margin: 2rem 0; color: #64748b; background: rgba(var(--primary-rgb), 0.05); border-radius: 0 1rem 1rem 0; }
|
|
326
|
+
.markdown-content pre { background: #0f172a; padding: 1.5rem; border-radius: 1.25rem; overflow-x: auto; font-family: 'Fira Code', monospace; margin: 2rem 0; line-height: 1.5; border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5); }
|
|
327
|
+
.markdown-content code { background: rgba(0,0,0,0.05); padding: 0.2rem 0.4rem; border-radius: 0.4rem; font-family: 'Fira Code', monospace; font-size: 0.9em; color: var(--primary); font-weight: 600; }
|
|
328
|
+
.markdown-content img { border-radius: 1.5rem; margin: 2rem auto; display: block; border: 1px solid rgba(0,0,0,0.05); transition: transform 0.3s ease; }
|
|
329
|
+
.markdown-content img:hover { transform: scale(1.01); }
|
|
330
|
+
.markdown-content table {
|
|
331
|
+
width: 100%;
|
|
332
|
+
border-collapse: separate;
|
|
333
|
+
border-spacing: 0;
|
|
334
|
+
margin: 2.5rem 0;
|
|
335
|
+
border: 1px solid rgba(0,0,0,0.1);
|
|
336
|
+
border-radius: 1.5rem;
|
|
337
|
+
overflow: hidden;
|
|
338
|
+
background: rgba(255, 255, 255, 0.4);
|
|
339
|
+
backdrop-filter: blur(10px);
|
|
340
|
+
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.1);
|
|
341
|
+
}
|
|
342
|
+
.markdown-content th {
|
|
343
|
+
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
|
344
|
+
color: white !important;
|
|
345
|
+
padding: 1.25rem 1.5rem;
|
|
346
|
+
font-weight: 800;
|
|
347
|
+
text-transform: uppercase;
|
|
348
|
+
font-size: 0.7rem;
|
|
349
|
+
text-align: left !important;
|
|
350
|
+
letter-spacing: 0.1em;
|
|
351
|
+
border: none;
|
|
352
|
+
}
|
|
353
|
+
.markdown-content td {
|
|
354
|
+
padding: 1.25rem 1.5rem;
|
|
355
|
+
border-bottom: 1px solid rgba(0,0,0,0.05);
|
|
356
|
+
font-size: 0.9rem;
|
|
357
|
+
color: #475569;
|
|
358
|
+
vertical-align: top;
|
|
359
|
+
line-height: 1.6;
|
|
360
|
+
}
|
|
361
|
+
.markdown-content tr:last-child td { border-bottom: none; }
|
|
362
|
+
.markdown-content tr:nth-child(even) { background: rgba(0,0,0,0.02); }
|
|
363
|
+
.markdown-content tr:hover { background: rgba(0,0,0,0.04); transition: background 0.3s; }
|
|
364
|
+
.markdown-content code { white-space: pre-wrap; word-break: break-word; }
|
|
365
|
+
.markdown-content pre code { white-space: pre; word-break: normal; }
|
|
366
|
+
.markdown-content a { color: var(--primary); font-weight: 700; text-decoration: none; border-bottom: 1px dashed var(--primary); transition: all 0.3s; }
|
|
367
|
+
.markdown-content a:hover { color: var(--secondary); border-bottom-style: solid; }
|
|
307
368
|
|
|
308
|
-
.dark .markdown-content
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
369
|
+
.dark .markdown-content table {
|
|
370
|
+
background: rgba(15, 23, 42, 0.6);
|
|
371
|
+
border-color: rgba(255,255,255,0.1);
|
|
372
|
+
box-shadow: 0 20px 40px -12px rgba(0,0,0,0.5);
|
|
373
|
+
}
|
|
374
|
+
.dark .markdown-content td {
|
|
375
|
+
border-color: rgba(255,255,255,0.05);
|
|
376
|
+
color: #94a3b8;
|
|
377
|
+
}
|
|
378
|
+
.dark .markdown-content tr:nth-child(even) { background: rgba(255,255,255,0.03); }
|
|
379
|
+
.dark .markdown-content tr:hover { background: rgba(255,255,255,0.05); }
|
|
313
380
|
|
|
314
381
|
/* Code Copy Button */
|
|
315
382
|
.code-wrapper { position: relative; margin: 1.5rem 0; }
|
|
@@ -498,7 +565,7 @@
|
|
|
498
565
|
</header>
|
|
499
566
|
|
|
500
567
|
<!-- Main Content Area -->
|
|
501
|
-
<main class="flex-1 md:ml-64 p-
|
|
568
|
+
<main class="flex-1 md:ml-64 p-4 md:p-12 pt-24 md:pt-12 min-w-0 w-full">
|
|
502
569
|
<div class="max-w-5xl mx-auto">
|
|
503
570
|
<%= @content %>
|
|
504
571
|
</div>
|
|
@@ -518,14 +585,14 @@
|
|
|
518
585
|
</div>
|
|
519
586
|
|
|
520
587
|
<ul class="hidden md:flex items-center gap-8">
|
|
521
|
-
<li><a href="/" class="nav-item <%= req.path == '/' ? 'active' : '' %>">Home</a></li>
|
|
588
|
+
<li><a href="/" class="nav-item <%= req.path == '/' ? 'active' : '' %>"><i class="fas fa-house mr-2 text-sm opacity-70"></i> Home</a></li>
|
|
522
589
|
<% if FEATURES_CONFIG['type'] == 'e_commerce' %>
|
|
523
|
-
<li><a href="/cart" class="nav-item <%= req.path == '/cart' ? 'active' : '' %>"><i class="fas fa-shopping-cart mr-
|
|
590
|
+
<li><a href="/cart" class="nav-item <%= req.path == '/cart' ? 'active' : '' %>"><i class="fas fa-shopping-cart mr-2 text-sm opacity-70"></i> Cart</a></li>
|
|
524
591
|
<% end %>
|
|
525
592
|
<% Page.where(is_nav: true, is_active: true).all.each do |p| %>
|
|
526
|
-
<li><a href="/<%= p.slug %>" class="nav-item <%= req.path == "/#{p.slug}" ? 'active' : '' %>"
|
|
593
|
+
<li><a href="/<%= p.slug %>" class="nav-item <%= req.path == "/#{p.slug}" ? 'active' : '' %>"><i class="fas fa-file-lines mr-2 text-sm opacity-70"></i> <%= p.title %></a></li>
|
|
527
594
|
<% end %>
|
|
528
|
-
<li><a href="/dashboard" class="nav-item <%= req.path.start_with?('/dashboard') ? 'active' : '' %>">Dashboard</a></li>
|
|
595
|
+
<li><a href="/dashboard" class="nav-item <%= req.path.start_with?('/dashboard') ? 'active' : '' %>"><i class="fas fa-gauge-high mr-2 text-sm opacity-70"></i> Dashboard</a></li>
|
|
529
596
|
</ul>
|
|
530
597
|
|
|
531
598
|
<div class="flex items-center gap-4">
|
|
@@ -561,7 +628,7 @@
|
|
|
561
628
|
|
|
562
629
|
<nav class="flex flex-col gap-6">
|
|
563
630
|
<a href="/" class="text-lg font-bold <%= req.path == '/' ? 'text-primary' : 'text-slate-500' %>">
|
|
564
|
-
<i class="fas fa-
|
|
631
|
+
<i class="fas fa-house w-8"></i> Home
|
|
565
632
|
</a>
|
|
566
633
|
<% if FEATURES_CONFIG['type'] == 'e_commerce' %>
|
|
567
634
|
<a href="/cart" class="text-lg font-bold <%= req.path == '/cart' ? 'text-primary' : 'text-slate-500' %>">
|
|
@@ -570,7 +637,7 @@
|
|
|
570
637
|
<% end %>
|
|
571
638
|
<% Page.where(is_nav: true, is_active: true).all.each do |p| %>
|
|
572
639
|
<a href="/<%= p.slug %>" class="text-lg font-bold <%= req.path == "/#{p.slug}" ? 'text-primary' : 'text-slate-500' %>">
|
|
573
|
-
<i class="fas fa-file-
|
|
640
|
+
<i class="fas fa-file-lines w-8"></i> <%= p.title %>
|
|
574
641
|
</a>
|
|
575
642
|
<% end %>
|
|
576
643
|
<div class="h-px <%= ['light_sidebar', 'light_glass'].include?(FEATURES_CONFIG['theme']) ? 'bg-black/10' : 'bg-white/5' %> my-2"></div>
|
data/app/views/page.erb
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
<div class="glass-card anime-element">
|
|
2
|
-
<
|
|
3
|
-
|
|
1
|
+
<div class="glass-card anime-element !p-5 md:!p-12 text-left max-w-4xl mx-auto">
|
|
2
|
+
<div class="mb-8 md:mb-12">
|
|
3
|
+
<div class="badge-premium">Halaman Statis</div>
|
|
4
|
+
<h1 class="text-2xl md:text-5xl font-black mb-4 leading-tight"><%= @title %></h1>
|
|
5
|
+
<div class="h-1 w-20 bg-primary rounded-full"></div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="page-content markdown-content">
|
|
4
9
|
<%= markdown(@content) %>
|
|
5
10
|
</div>
|
|
11
|
+
|
|
6
12
|
<div class="mt-12 pt-8 border-t border-white/5">
|
|
7
|
-
<a href="/" class="btn-secondary">
|
|
8
|
-
<i class="fas fa-arrow-left mr-2"></i>
|
|
13
|
+
<a href="/" class="btn-secondary w-full sm:w-auto">
|
|
14
|
+
<i class="fas fa-arrow-left mr-2"></i> Kembali ke Beranda
|
|
9
15
|
</a>
|
|
10
16
|
</div>
|
|
11
17
|
</div>
|
data/app/views/post.erb
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
<div class="glass-card anime-element
|
|
2
|
-
<div
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
<div class="glass-card anime-element !p-6 md:!p-12 text-left max-w-4xl mx-auto">
|
|
2
|
+
<div class="mb-8 md:mb-12">
|
|
3
|
+
<div class="badge-premium"><%= @post.category %></div>
|
|
4
|
+
<h1 class="text-3xl md:text-5xl font-black mb-4 leading-tight"><%= @post.title %></h1>
|
|
5
|
+
<p class="text-slate-500 dark:text-slate-400 text-sm md:text-base flex items-center gap-2">
|
|
6
|
+
<i class="far fa-calendar-alt"></i> Diterbitkan pada <%= @post.created_at.strftime('%d %B %Y') %>
|
|
7
|
+
</p>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<% if @post.image_url && !@post.image_url.empty? %>
|
|
11
|
-
<
|
|
11
|
+
<div class="relative rounded-2xl md:rounded-3xl overflow-hidden mb-8 md:mb-12 border border-white/10">
|
|
12
|
+
<img src="<%= @post.image_url %>" alt="<%= @post.title %>" class="w-full h-auto max-h-[500px] object-cover">
|
|
13
|
+
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
|
14
|
+
</div>
|
|
12
15
|
<% end %>
|
|
13
16
|
|
|
14
17
|
<div class="post-content markdown-content">
|
|
@@ -16,8 +19,8 @@
|
|
|
16
19
|
</div>
|
|
17
20
|
|
|
18
21
|
<div class="mt-12 pt-8 border-t border-white/5">
|
|
19
|
-
<a href="/" class="btn-secondary">
|
|
20
|
-
<i class="fas fa-arrow-left mr-2"></i>
|
|
22
|
+
<a href="/" class="btn-secondary w-full sm:w-auto">
|
|
23
|
+
<i class="fas fa-arrow-left mr-2"></i> Kembali ke Blog
|
|
21
24
|
</a>
|
|
22
25
|
</div>
|
|
23
26
|
</div>
|
data/app/views/project.erb
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
<div class="glass-card anime-element
|
|
2
|
-
<div
|
|
3
|
-
<
|
|
4
|
-
<
|
|
1
|
+
<div class="glass-card anime-element !p-5 md:!p-12 text-left max-w-4xl mx-auto">
|
|
2
|
+
<div class="mb-8 md:mb-12">
|
|
3
|
+
<div class="badge-premium">Detail Proyek</div>
|
|
4
|
+
<h1 class="text-2xl md:text-5xl font-black mb-4 leading-tight"><%= @project.title %></h1>
|
|
5
|
+
<p class="text-slate-500 dark:text-slate-400 text-sm md:text-base flex items-center gap-2">
|
|
6
|
+
<i class="far fa-calendar-alt"></i> Dibuat pada <%= @project.created_at.strftime('%d %B %Y') %>
|
|
7
|
+
</p>
|
|
5
8
|
</div>
|
|
6
9
|
|
|
7
10
|
<% if @project.image_url && !@project.image_url.empty? %>
|
|
8
|
-
<
|
|
11
|
+
<div class="relative rounded-2xl md:rounded-3xl overflow-hidden mb-8 md:mb-12 border border-white/10">
|
|
12
|
+
<img src="<%= @project.image_url %>" alt="<%= @project.title %>" class="w-full h-auto max-h-[500px] object-cover">
|
|
13
|
+
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
|
14
|
+
</div>
|
|
9
15
|
<% else %>
|
|
10
|
-
<div
|
|
11
|
-
|
|
16
|
+
<div class="w-full aspect-video bg-gradient-to-br from-primary/20 to-secondary/20 rounded-2xl md:rounded-3xl mb-8 md:mb-12 flex items-center justify-center border border-white/10">
|
|
17
|
+
<i class="fas fa-project-diagram text-4xl md:text-6xl text-primary/40"></i>
|
|
12
18
|
</div>
|
|
13
19
|
<% end %>
|
|
14
20
|
|
|
@@ -16,14 +22,14 @@
|
|
|
16
22
|
<%= markdown(@project.description) %>
|
|
17
23
|
</div>
|
|
18
24
|
|
|
19
|
-
<div class="flex flex-
|
|
25
|
+
<div class="flex flex-col sm:flex-row gap-4 items-center mt-12 pt-8 border-t border-white/5">
|
|
20
26
|
<% if @project.link && !@project.link.empty? %>
|
|
21
|
-
<a href="<%= @project.link %>" target="_blank" class="btn-premium">
|
|
22
|
-
<i class="fas fa-
|
|
27
|
+
<a href="<%= @project.link %>" target="_blank" class="btn-premium w-full sm:w-auto">
|
|
28
|
+
<i class="fas fa-external-link-alt mr-2"></i> Kunjungi Demo
|
|
23
29
|
</a>
|
|
24
30
|
<% end %>
|
|
25
|
-
<a href="/" class="btn-secondary">
|
|
26
|
-
<i class="fas fa-arrow-left mr-2"></i>
|
|
31
|
+
<a href="/" class="btn-secondary w-full sm:w-auto">
|
|
32
|
+
<i class="fas fa-arrow-left mr-2"></i> Kembali ke Beranda
|
|
27
33
|
</a>
|
|
28
34
|
</div>
|
|
29
35
|
</div>
|
data/bin/ofa
CHANGED
|
@@ -33,7 +33,7 @@ def help
|
|
|
33
33
|
puts " / __ \\/ ____/ / | "
|
|
34
34
|
puts " / / / / /_ / /| | Framework "
|
|
35
35
|
puts "/ /_/ / __/ / ___ | Premium MVC "
|
|
36
|
-
puts "\\____/_/ /_/ |_| v5.
|
|
36
|
+
puts "\\____/_/ /_/ |_| v5.3.0 "
|
|
37
37
|
puts " "
|
|
38
38
|
puts "✨ One-For-All Framework CLI ✨"
|
|
39
39
|
puts "-----------------------------"
|
|
@@ -93,6 +93,7 @@ end
|
|
|
93
93
|
def perform_db_migration(target_type, target_name)
|
|
94
94
|
# 1. Initialize source environment
|
|
95
95
|
Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
|
|
96
|
+
ENV['SKIP_MODELS'] = '1'
|
|
96
97
|
require File.join(FRAMEWORK_ROOT, 'config', 'boot')
|
|
97
98
|
source_db = DB
|
|
98
99
|
|
|
@@ -196,7 +197,7 @@ def perform_db_migration(target_type, target_name)
|
|
|
196
197
|
|
|
197
198
|
# 5. Update configuration to point to new DB
|
|
198
199
|
absolute_ofa_path = File.expand_path(__FILE__)
|
|
199
|
-
system("ruby #{absolute_ofa_path} db switch #{target_type} #{target_name}")
|
|
200
|
+
system("ruby #{absolute_ofa_path} db switch #{target_type} \"#{target_name}\"")
|
|
200
201
|
|
|
201
202
|
puts "✅ Migration successful!"
|
|
202
203
|
end
|
|
@@ -248,7 +249,7 @@ when 'init'
|
|
|
248
249
|
puts " / __ \\/ ____/ / | "
|
|
249
250
|
puts " / / / / /_ / /| | Framework "
|
|
250
251
|
puts "/ /_/ / __/ / ___ | Premium MVC "
|
|
251
|
-
puts "\\____/_/ /_/ |_| v5.
|
|
252
|
+
puts "\\____/_/ /_/ |_| v5.3.0 "
|
|
252
253
|
puts " "
|
|
253
254
|
puts "Initializing One-For-All project as '#{app_type}' in #{PROJECT_ROOT}..."
|
|
254
255
|
|
|
@@ -306,6 +307,16 @@ when 'init'
|
|
|
306
307
|
end
|
|
307
308
|
end
|
|
308
309
|
|
|
310
|
+
# Standard project files
|
|
311
|
+
['.env.example', '.gitignore', 'Dockerfile', 'LICENSE', 'README.md', 'Procfile'].each do |file|
|
|
312
|
+
src = File.join(FRAMEWORK_ROOT, file)
|
|
313
|
+
dest = File.join(PROJECT_ROOT, file)
|
|
314
|
+
if File.exist?(src)
|
|
315
|
+
FileUtils.cp(src, dest)
|
|
316
|
+
puts " Creating #{file}..."
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
309
320
|
# Create .env
|
|
310
321
|
env_content = []
|
|
311
322
|
env_content << "EKS_ENV=development"
|
|
@@ -320,8 +331,16 @@ when 'init'
|
|
|
320
331
|
# Generated by One-For-All CLI
|
|
321
332
|
# Target framework: #{FRAMEWORK_ROOT}
|
|
322
333
|
|
|
323
|
-
# Add framework to load path
|
|
324
|
-
|
|
334
|
+
# Add framework to load path dynamically
|
|
335
|
+
require 'bundler/setup'
|
|
336
|
+
framework_spec = Gem.loaded_specs['one-for-all-framework']
|
|
337
|
+
if framework_spec
|
|
338
|
+
$LOAD_PATH.unshift framework_spec.full_gem_path
|
|
339
|
+
else
|
|
340
|
+
# Fallback for local development if not in bundle (though it should be)
|
|
341
|
+
$LOAD_PATH.unshift '#{FRAMEWORK_ROOT}'
|
|
342
|
+
end
|
|
343
|
+
|
|
325
344
|
APP_ROOT = File.expand_path(__dir__)
|
|
326
345
|
require 'config/boot'
|
|
327
346
|
|
|
@@ -346,6 +365,7 @@ when 'init'
|
|
|
346
365
|
gemfile_content = <<~RUBY
|
|
347
366
|
source 'https://rubygems.org'
|
|
348
367
|
|
|
368
|
+
gem 'one-for-all-framework', '5.3.0'
|
|
349
369
|
gem 'eks-cent', '4.0.0'
|
|
350
370
|
gem 'eksa-server'
|
|
351
371
|
gem 'sequel'
|
|
@@ -861,7 +881,7 @@ when 'console'
|
|
|
861
881
|
puts " / __ \\/ ____/ / | "
|
|
862
882
|
puts " / / / / /_ / /| | Framework "
|
|
863
883
|
puts "/ /_/ / __/ / ___ | Console (REPL) "
|
|
864
|
-
puts "\\____/_/ /_/ |_| v5.
|
|
884
|
+
puts "\\____/_/ /_/ |_| v5.3.0 "
|
|
865
885
|
puts " "
|
|
866
886
|
puts "✨ Loading environment... (Type 'exit' to quit)"
|
|
867
887
|
|
data/config/boot.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
Encoding.default_external = Encoding::UTF_8
|
|
2
|
+
Encoding.default_internal = Encoding::UTF_8
|
|
1
3
|
require 'bundler/setup'
|
|
2
4
|
require 'dotenv/load'
|
|
3
5
|
Bundler.require(:default)
|
|
@@ -40,7 +42,7 @@ module EksCent
|
|
|
40
42
|
raise "Template not found: #{template_path}"
|
|
41
43
|
end
|
|
42
44
|
|
|
43
|
-
template_content = File.read(template_path)
|
|
45
|
+
template_content = File.read(template_path, encoding: 'UTF-8')
|
|
44
46
|
context = Object.new
|
|
45
47
|
context.extend(ERB::Util)
|
|
46
48
|
req = @request
|
|
@@ -64,17 +66,20 @@ module EksCent
|
|
|
64
66
|
|
|
65
67
|
context.define_singleton_method(:session) { req ? (req.env['eks_cent.session'] || req.env['rack.session'] || {}) : {} }
|
|
66
68
|
context.define_singleton_method(:h) { |s| CGI.escapeHTML(s.to_s) }
|
|
67
|
-
locals.each
|
|
69
|
+
locals.each do |k, v|
|
|
70
|
+
v = v.force_encoding('UTF-8') if v.is_a?(String) && v.encoding == Encoding::BINARY
|
|
71
|
+
context.instance_variable_set("@#{k}", v)
|
|
72
|
+
end
|
|
68
73
|
|
|
69
|
-
result = ERB.new(template_content).result(context.instance_eval { binding })
|
|
74
|
+
result = ERB.new(template_content).result(context.instance_eval { binding }).force_encoding('UTF-8')
|
|
70
75
|
|
|
71
76
|
if layout
|
|
72
77
|
layout_name = layout == true ? 'layout' : layout.to_s
|
|
73
78
|
layout_path = File.join(APP_ROOT, 'app', 'views', "#{layout_name}.erb")
|
|
74
79
|
if File.file?(layout_path)
|
|
75
80
|
context.instance_variable_set("@content", result)
|
|
76
|
-
layout_content = File.read(layout_path)
|
|
77
|
-
result = ERB.new(layout_content).result(context.instance_eval { binding })
|
|
81
|
+
layout_content = File.read(layout_path, encoding: 'UTF-8')
|
|
82
|
+
result = ERB.new(layout_content).result(context.instance_eval { binding }).force_encoding('UTF-8')
|
|
78
83
|
end
|
|
79
84
|
end
|
|
80
85
|
|
data/config/features.json
CHANGED
data/db/data.sqlite3
CHANGED
|
Binary file
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
|
|
3
|
+
class FeaturesTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
# Clear logs and maintenance mode before each test
|
|
6
|
+
ActivityLog.dataset.delete rescue nil
|
|
7
|
+
config_path = File.join(APP_ROOT, 'config', 'features.json')
|
|
8
|
+
config = JSON.parse(File.read(config_path))
|
|
9
|
+
config['maintenance'] = false
|
|
10
|
+
File.write(config_path, JSON.pretty_generate(config))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_maintenance_mode_toggle
|
|
14
|
+
# Turn ON
|
|
15
|
+
system("ruby bin/ofa maintenance on > /dev/null")
|
|
16
|
+
config = JSON.parse(File.read(File.join(APP_ROOT, 'config', 'features.json')))
|
|
17
|
+
assert_equal true, config['maintenance']
|
|
18
|
+
|
|
19
|
+
# Turn OFF
|
|
20
|
+
system("ruby bin/ofa maintenance off > /dev/null")
|
|
21
|
+
config = JSON.parse(File.read(File.join(APP_ROOT, 'config', 'features.json')))
|
|
22
|
+
assert_equal false, config['maintenance']
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_activity_logging
|
|
26
|
+
# Test logging manually
|
|
27
|
+
ActivityLog.log(1, "Test Action", nil, "Details here")
|
|
28
|
+
log = ActivityLog.order(:created_at).last
|
|
29
|
+
|
|
30
|
+
assert_equal "Test Action", log.action
|
|
31
|
+
assert_equal "1", log.user_id
|
|
32
|
+
assert_equal "Details here", log.details
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_plugin_generator
|
|
36
|
+
plugin_name = "test_plugin_#{Time.now.to_i}"
|
|
37
|
+
plugin_path = File.join(APP_ROOT, 'plugins', plugin_name)
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
system("ruby bin/ofa g plugin #{plugin_name} > /dev/null")
|
|
41
|
+
assert Dir.exist?(plugin_path)
|
|
42
|
+
assert File.exist?(File.join(plugin_path, "init.rb"))
|
|
43
|
+
ensure
|
|
44
|
+
FileUtils.rm_rf(plugin_path) if Dir.exist?(plugin_path)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_maintenance_middleware_blocks_access
|
|
49
|
+
# Mock environment
|
|
50
|
+
env = {
|
|
51
|
+
'PATH_INFO' => '/',
|
|
52
|
+
'eks_cent.session' => {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# 1. Test when maintenance is OFF
|
|
56
|
+
FEATURES_CONFIG['maintenance'] = false
|
|
57
|
+
app = lambda { |e| [200, {}, ['OK']] }
|
|
58
|
+
middleware = MaintenanceMiddleware.new(app)
|
|
59
|
+
status, _, _ = middleware.call(env)
|
|
60
|
+
assert_equal 200, status
|
|
61
|
+
|
|
62
|
+
# 2. Test when maintenance is ON (should block)
|
|
63
|
+
config_path = File.join(APP_ROOT, 'config', 'features.json')
|
|
64
|
+
config = JSON.parse(File.read(config_path))
|
|
65
|
+
config['maintenance'] = true
|
|
66
|
+
File.write(config_path, JSON.pretty_generate(config))
|
|
67
|
+
|
|
68
|
+
status, _, _ = middleware.call(env)
|
|
69
|
+
assert_equal 503, status
|
|
70
|
+
|
|
71
|
+
# 3. Test when maintenance is ON but allowed path
|
|
72
|
+
env['PATH_INFO'] = '/login'
|
|
73
|
+
status, _, _ = middleware.call(env)
|
|
74
|
+
assert_equal 200, status
|
|
75
|
+
|
|
76
|
+
# 4. Test when maintenance is ON but admin is logged in
|
|
77
|
+
env['PATH_INFO'] = '/'
|
|
78
|
+
env['eks_cent.session'] = { 'user_id' => 1 }
|
|
79
|
+
status, _, _ = middleware.call(env)
|
|
80
|
+
assert_equal 200, status
|
|
81
|
+
|
|
82
|
+
# Cleanup
|
|
83
|
+
FEATURES_CONFIG['maintenance'] = false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require 'eksa-mination'
|
|
2
|
+
require 'rack'
|
|
3
|
+
require 'rack/test'
|
|
4
|
+
require_relative '../config/boot'
|
|
5
|
+
|
|
6
|
+
describe "One-For-All Framework" do
|
|
7
|
+
before do
|
|
8
|
+
extend Rack::Test::Methods
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
let(:app) do
|
|
12
|
+
Rack::Builder.new do
|
|
13
|
+
use EksCent::Middleware::Session, secret: EksCent.secret_key_base
|
|
14
|
+
use AuthMiddleware
|
|
15
|
+
use CSRFMiddleware
|
|
16
|
+
use EksCent::Middleware::Logger
|
|
17
|
+
run ROUTES
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "menampilkan halaman utama dengan benar" do
|
|
22
|
+
get '/'
|
|
23
|
+
expect(last_response.status).to eq(200)
|
|
24
|
+
expect(last_response.body).to include("One-For-All")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "menampilkan halaman login" do
|
|
28
|
+
get '/login'
|
|
29
|
+
expect(last_response.status).to eq(200)
|
|
30
|
+
expect(last_response.body).to include("Welcome Back")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "menolak request POST tanpa CSRF token" do
|
|
34
|
+
post '/login', { username: 'admin', password: 'pwd' }
|
|
35
|
+
expect(last_response.status).to eq(403)
|
|
36
|
+
expect(last_response.body).to include("CSRF Token Invalid")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "mengarahkan rute terproteksi ke halaman login jika belum autentikasi" do
|
|
40
|
+
get '/dashboard'
|
|
41
|
+
expect(last_response.status).to eq(302)
|
|
42
|
+
expect(last_response.location).to match(/\/login$/)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "merespon endpoint API status dengan JSON" do
|
|
46
|
+
get '/api/status'
|
|
47
|
+
expect(last_response.status).to eq(200)
|
|
48
|
+
expect(last_response.body).to include("ok")
|
|
49
|
+
expect(last_response.headers['Content-Type']).to eq('application/json')
|
|
50
|
+
end
|
|
51
|
+
end
|
data/test/ofa_spec.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
require 'eksa-mination'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'sequel'
|
|
5
|
+
require 'bcrypt'
|
|
6
|
+
|
|
7
|
+
# Backup & Restore Logic outside DSL to ensure execution
|
|
8
|
+
CONFIG_FILES = ["config/database.json", "config/features.json"]
|
|
9
|
+
CONFIG_FILES.each do |file|
|
|
10
|
+
FileUtils.cp(file, "#{file}.bak") if File.exist?(file)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
at_exit do
|
|
14
|
+
CONFIG_FILES.each do |file|
|
|
15
|
+
if File.exist?("#{file}.bak")
|
|
16
|
+
FileUtils.mv("#{file}.bak", file)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
FileUtils.rm("db/test.sqlite3") if File.exist?("db/test.sqlite3")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "Ofa CLI" do
|
|
23
|
+
let(:ofa) { File.join(Dir.pwd, "bin", "ofa") }
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
# Setting lingkungan test yang konsisten
|
|
27
|
+
`./bin/ofa db switch sqlite db/test.sqlite3`
|
|
28
|
+
`./bin/ofa feature enable auth`
|
|
29
|
+
`./bin/ofa type landing_page`
|
|
30
|
+
`./bin/ofa theme dark_glass`
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "menampilkan bantuan dengan benar" do
|
|
34
|
+
output = `./bin/ofa help`
|
|
35
|
+
expect(output).to include("One-For-All Framework CLI")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "dapat meng-generate controller" do
|
|
39
|
+
`./bin/ofa g controller Blog`
|
|
40
|
+
expect(File.exist?("app/controllers/blog_controller.rb")).to be_truthy
|
|
41
|
+
expect(File.read("app/controllers/blog_controller.rb")).to include("class BlogController")
|
|
42
|
+
FileUtils.rm("app/controllers/blog_controller.rb")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "dapat mengubah fitur (enable/disable)" do
|
|
46
|
+
config_path = "config/features.json"
|
|
47
|
+
`./bin/ofa feature disable auth`
|
|
48
|
+
new_config = JSON.parse(File.read(config_path))
|
|
49
|
+
expect(new_config["auth"]).to be_falsey
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "dapat mengganti tipe aplikasi" do
|
|
53
|
+
config_path = "config/features.json"
|
|
54
|
+
`./bin/ofa type portfolio`
|
|
55
|
+
config = JSON.parse(File.read(config_path))
|
|
56
|
+
expect(config["type"]).to eq("portfolio")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "dapat mengganti tema aplikasi" do
|
|
60
|
+
config_path = "config/features.json"
|
|
61
|
+
`./bin/ofa theme light_glass`
|
|
62
|
+
config = JSON.parse(File.read(config_path))
|
|
63
|
+
expect(config["theme"]).to eq("light_glass")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "dapat mengganti konfigurasi database" do
|
|
67
|
+
config_path = "config/database.json"
|
|
68
|
+
`./bin/ofa db switch postgres production_db`
|
|
69
|
+
config = JSON.parse(File.read(config_path))
|
|
70
|
+
expect(config["adapter"]).to eq("postgres")
|
|
71
|
+
expect(config["database"]).to eq("production_db")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "dapat mereset password admin (dan membuat user jika belum ada)" do
|
|
75
|
+
username = "test_admin_#{Time.now.to_i}"
|
|
76
|
+
strong_password = "Secret123" # Memenuhi: 8+ karakter, huruf kapital, angka
|
|
77
|
+
`./bin/ofa reset-password #{username} #{strong_password}`
|
|
78
|
+
|
|
79
|
+
# Verifikasi langsung ke file DB
|
|
80
|
+
test_db = Sequel.connect("sqlite://db/test.sqlite3")
|
|
81
|
+
user = test_db[:users].first(username: username)
|
|
82
|
+
expect(user).not_to be_nil
|
|
83
|
+
expect(BCrypt::Password.new(user[:password_hash]) == strong_password).to be_truthy
|
|
84
|
+
test_db.disconnect
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "dapat meng-generate model baru" do
|
|
88
|
+
`./bin/ofa g model TestItem`
|
|
89
|
+
expect(File.exist?("app/models/testitem.rb")).to be_truthy
|
|
90
|
+
expect(File.read("app/models/testitem.rb")).to include("class Testitem < Sequel::Model")
|
|
91
|
+
FileUtils.rm("app/models/testitem.rb")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "dapat meng-generate migration baru" do
|
|
95
|
+
`./bin/ofa g migration create_tests`
|
|
96
|
+
migrations = Dir.glob("db/migrations/*_create_tests.rb")
|
|
97
|
+
expect(migrations.any?).to be_truthy
|
|
98
|
+
migrations.each { |f| FileUtils.rm(f) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "dapat meng-generate post blog" do
|
|
102
|
+
`./bin/ofa g post "Testing Post" --author Antigravity --category Tech`
|
|
103
|
+
expect(File.exist?("app/views/posts/testing_post.erb")).to be_truthy
|
|
104
|
+
expect(File.read("app/views/posts/testing_post.erb")).to include("Testing Post")
|
|
105
|
+
FileUtils.rm("app/views/posts/testing_post.erb")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "dapat mengganti konfigurasi storage" do
|
|
109
|
+
config_path = "config/features.json"
|
|
110
|
+
`./bin/ofa storage cloudinary`
|
|
111
|
+
config = JSON.parse(File.read(config_path))
|
|
112
|
+
expect(config["storage"]).to eq("cloudinary")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "dapat melakukan migrasi data antar database (SQLite)" do
|
|
116
|
+
source_db_path = "db/source_test.sqlite3"
|
|
117
|
+
target_db_path = "db/target_test.sqlite3"
|
|
118
|
+
FileUtils.rm(source_db_path) if File.exist?(source_db_path)
|
|
119
|
+
FileUtils.rm(target_db_path) if File.exist?(target_db_path)
|
|
120
|
+
|
|
121
|
+
`./bin/ofa db switch sqlite #{source_db_path}`
|
|
122
|
+
username = "migrator_user"
|
|
123
|
+
`./bin/ofa reset-password #{username} Secret123`
|
|
124
|
+
|
|
125
|
+
`./bin/ofa db migrate-data sqlite #{target_db_path}`
|
|
126
|
+
|
|
127
|
+
expect(File.exist?(target_db_path)).to be_truthy
|
|
128
|
+
target_db = Sequel.connect("sqlite://#{target_db_path}")
|
|
129
|
+
user = target_db[:users].first(username: username)
|
|
130
|
+
expect(user).not_to be_nil
|
|
131
|
+
target_db.disconnect
|
|
132
|
+
|
|
133
|
+
FileUtils.rm(source_db_path) if File.exist?(source_db_path)
|
|
134
|
+
FileUtils.rm(target_db_path) if File.exist?(target_db_path)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "dapat meng-generate API controller baru" do
|
|
138
|
+
`./bin/ofa g api Shop`
|
|
139
|
+
expect(File.exist?("app/controllers/shop_controller.rb")).to be_truthy
|
|
140
|
+
content = File.read("app/controllers/shop_controller.rb")
|
|
141
|
+
expect(content).to include("class ShopController < ApiController")
|
|
142
|
+
expect(content).to include("render_json")
|
|
143
|
+
FileUtils.rm("app/controllers/shop_controller.rb")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "dapat meng-generate dokumentasi Swagger/OpenAPI" do
|
|
147
|
+
`./bin/ofa swagger`
|
|
148
|
+
expect(File.exist?("openapi.json")).to be_truthy
|
|
149
|
+
config = JSON.parse(File.read("openapi.json"))
|
|
150
|
+
expect(config["openapi"]).to eq("3.0.0")
|
|
151
|
+
expect(config["paths"].any?).to be_truthy
|
|
152
|
+
FileUtils.rm("openapi.json")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "dapat menampilkan daftar rute" do
|
|
156
|
+
output = `./bin/ofa routes`
|
|
157
|
+
expect(output).to include("Registered Routes")
|
|
158
|
+
expect(output).to include("/api/status")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "dapat menjalankan pemeriksaan kesehatan sistem (doctor)" do
|
|
162
|
+
output = `./bin/ofa doctor`
|
|
163
|
+
expect(output).to include("One-For-All Doctor")
|
|
164
|
+
expect(output).to include("Checking .env file")
|
|
165
|
+
expect(output).to include("Checking Database connection")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "dapat meng-generate mailer baru" do
|
|
169
|
+
`./bin/ofa g mailer Welcome signup`
|
|
170
|
+
expect(File.exist?("app/mailers/welcome_mailer.rb")).to be_truthy
|
|
171
|
+
expect(File.exist?("app/views/mailers/welcome_mailer/signup.erb")).to be_truthy
|
|
172
|
+
FileUtils.rm("app/mailers/welcome_mailer.rb")
|
|
173
|
+
FileUtils.rm_rf("app/views/mailers/welcome_mailer")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "dapat meng-generate task baru" do
|
|
177
|
+
`./bin/ofa g task Cleanup`
|
|
178
|
+
expect(File.exist?("lib/tasks/cleanup.rb")).to be_truthy
|
|
179
|
+
FileUtils.rm("lib/tasks/cleanup.rb")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "dapat menjalankan task yang didefinisikan" do
|
|
183
|
+
File.write("lib/tasks/hello.rb", "task :hello do; puts 'Hello Task'; end")
|
|
184
|
+
output = `./bin/ofa task hello`
|
|
185
|
+
expect(output).to include("Running task: hello")
|
|
186
|
+
expect(output).to include("Hello Task")
|
|
187
|
+
FileUtils.rm("lib/tasks/hello.rb")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "dapat meng-generate test baru" do
|
|
191
|
+
`./bin/ofa g test unit`
|
|
192
|
+
expect(File.exist?("test/unit_test.rb")).to be_truthy
|
|
193
|
+
FileUtils.rm("test/unit_test.rb")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "dapat menjalankan perintah test suite" do
|
|
197
|
+
# Buat dummy test agar tidak error 'No tests found'
|
|
198
|
+
dummy_file = "test/z_dummy_test.rb"
|
|
199
|
+
File.write(dummy_file, "require_relative 'test_helper'\nclass DummyTest < Minitest::Test; def test_pass; assert true; end; end")
|
|
200
|
+
output = `./bin/ofa test #{dummy_file}`
|
|
201
|
+
expect(output).to include("Running tests")
|
|
202
|
+
expect(output).to include("1 runs, 1 assertions")
|
|
203
|
+
FileUtils.rm(dummy_file) if File.exist?(dummy_file)
|
|
204
|
+
end
|
|
205
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ENV['EKS_ENV'] = 'test'
|
|
2
|
+
require 'minitest/autorun'
|
|
3
|
+
require 'minitest/pride' # Nice colors
|
|
4
|
+
|
|
5
|
+
# Load the application environment
|
|
6
|
+
require_relative '../config/boot'
|
|
7
|
+
|
|
8
|
+
class Minitest::Test
|
|
9
|
+
# Add helper methods for tests here
|
|
10
|
+
def setup
|
|
11
|
+
# Run migrations or setup DB for tests
|
|
12
|
+
end
|
|
13
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: one-for-all-framework
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ishikawa Uta
|
|
@@ -268,6 +268,10 @@ files:
|
|
|
268
268
|
- public/css/cms.css
|
|
269
269
|
- public/images/logo.jpg
|
|
270
270
|
- public/images/logo.png
|
|
271
|
+
- test/features_test.rb
|
|
272
|
+
- test/framework_spec.rb
|
|
273
|
+
- test/ofa_spec.rb
|
|
274
|
+
- test/test_helper.rb
|
|
271
275
|
homepage: https://github.com/ishikawauta/one-for-all-framework
|
|
272
276
|
licenses:
|
|
273
277
|
- MIT
|